@mmmbuto/anthmorph 0.1.0
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/Cargo.lock +1935 -0
- package/Cargo.toml +61 -0
- package/LICENSE +21 -0
- package/README.md +184 -0
- package/bin/anthmorph +22 -0
- package/package.json +52 -0
- package/scripts/anthmorphctl +456 -0
- package/scripts/postinstall.js +23 -0
- package/scripts/smoke_test.sh +72 -0
- package/src/config.rs +39 -0
- package/src/error.rs +54 -0
- package/src/main.rs +120 -0
- package/src/models/anthropic.rs +274 -0
- package/src/models/mod.rs +2 -0
- package/src/models/openai.rs +230 -0
- package/src/proxy.rs +829 -0
- package/src/transform.rs +460 -0
- package/tests/real_backends.rs +213 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
|
5
|
+
STATE_DIR=${ANTHMORPH_STATE_DIR:-$ROOT_DIR/.anthmorph}
|
|
6
|
+
CONFIG_FILE=$STATE_DIR/config.env
|
|
7
|
+
PID_FILE=$STATE_DIR/anthmorph.pid
|
|
8
|
+
LOG_FILE=$STATE_DIR/anthmorph.log
|
|
9
|
+
BIN_PATH_DEFAULT=$ROOT_DIR/target/release/anthmorph
|
|
10
|
+
|
|
11
|
+
mkdir -p "$STATE_DIR"
|
|
12
|
+
|
|
13
|
+
usage() {
|
|
14
|
+
cat <<USAGE
|
|
15
|
+
usage:
|
|
16
|
+
scripts/anthmorphctl init chutes [options]
|
|
17
|
+
scripts/anthmorphctl init minimax [options]
|
|
18
|
+
scripts/anthmorphctl init openai --backend-url URL --model MODEL [options]
|
|
19
|
+
scripts/anthmorphctl set key-env ENV_NAME
|
|
20
|
+
scripts/anthmorphctl set key VALUE --save
|
|
21
|
+
scripts/anthmorphctl unset key
|
|
22
|
+
scripts/anthmorphctl start
|
|
23
|
+
scripts/anthmorphctl stop
|
|
24
|
+
scripts/anthmorphctl restart
|
|
25
|
+
scripts/anthmorphctl status
|
|
26
|
+
scripts/anthmorphctl logs
|
|
27
|
+
scripts/anthmorphctl print-config
|
|
28
|
+
scripts/anthmorphctl help
|
|
29
|
+
|
|
30
|
+
options for init:
|
|
31
|
+
--port PORT
|
|
32
|
+
--backend-url URL
|
|
33
|
+
--model MODEL
|
|
34
|
+
--reasoning-model MODEL
|
|
35
|
+
--ingress-api-key VALUE
|
|
36
|
+
--allow-origin ORIGIN
|
|
37
|
+
--key-env ENV_NAME
|
|
38
|
+
--api-key VALUE --save-key
|
|
39
|
+
|
|
40
|
+
notes:
|
|
41
|
+
- by default the API key is not saved; use set key-env or --key-env
|
|
42
|
+
- set key VALUE --save stores the key in the local config file
|
|
43
|
+
- config lives in .anthmorph/config.env
|
|
44
|
+
USAGE
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
ensure_config() {
|
|
48
|
+
if [ ! -f "$CONFIG_FILE" ]; then
|
|
49
|
+
echo "missing config: run 'scripts/anthmorphctl init ...' first" >&2
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
load_config() {
|
|
55
|
+
ensure_config
|
|
56
|
+
# shellcheck disable=SC1090
|
|
57
|
+
. "$CONFIG_FILE"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
save_config() {
|
|
61
|
+
tmp_file=$CONFIG_FILE.tmp
|
|
62
|
+
{
|
|
63
|
+
printf 'BACKEND_PROFILE=%s\n' "${BACKEND_PROFILE:-}"
|
|
64
|
+
printf 'BACKEND_URL=%s\n' "${BACKEND_URL:-}"
|
|
65
|
+
printf 'MODEL=%s\n' "${MODEL:-}"
|
|
66
|
+
printf 'REASONING_MODEL=%s\n' "${REASONING_MODEL:-}"
|
|
67
|
+
printf 'PORT=%s\n' "${PORT:-3000}"
|
|
68
|
+
printf 'INGRESS_API_KEY=%s\n' "${INGRESS_API_KEY:-}"
|
|
69
|
+
printf 'ALLOWED_ORIGINS=%s\n' "${ALLOWED_ORIGINS:-}"
|
|
70
|
+
printf 'API_KEY_ENV=%s\n' "${API_KEY_ENV:-}"
|
|
71
|
+
printf 'API_KEY=%s\n' "${API_KEY:-}"
|
|
72
|
+
printf 'BIN_PATH=%s\n' "${BIN_PATH:-$BIN_PATH_DEFAULT}"
|
|
73
|
+
} > "$tmp_file"
|
|
74
|
+
mv "$tmp_file" "$CONFIG_FILE"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
is_running() {
|
|
78
|
+
[ -f "$PID_FILE" ] || return 1
|
|
79
|
+
pid=$(cat "$PID_FILE" 2>/dev/null || true)
|
|
80
|
+
[ -n "$pid" ] || return 1
|
|
81
|
+
kill -0 "$pid" 2>/dev/null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
resolve_api_key() {
|
|
85
|
+
if [ -n "${API_KEY:-}" ]; then
|
|
86
|
+
printf '%s' "$API_KEY"
|
|
87
|
+
return 0
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
if [ -n "${API_KEY_ENV:-}" ]; then
|
|
91
|
+
eval "resolved=\${$API_KEY_ENV-}"
|
|
92
|
+
if [ -n "${resolved:-}" ]; then
|
|
93
|
+
printf '%s' "$resolved"
|
|
94
|
+
return 0
|
|
95
|
+
fi
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
return 1
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
health_on_port() {
|
|
102
|
+
curl -fsS "http://127.0.0.1:$1/health" 2>/dev/null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
join_allow_origin() {
|
|
106
|
+
value=$1
|
|
107
|
+
if [ -z "${ALLOWED_ORIGINS:-}" ]; then
|
|
108
|
+
ALLOWED_ORIGINS=$value
|
|
109
|
+
else
|
|
110
|
+
ALLOWED_ORIGINS=${ALLOWED_ORIGINS},$value
|
|
111
|
+
fi
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
parse_common_init_args() {
|
|
115
|
+
SAVE_KEY=0
|
|
116
|
+
while [ "$#" -gt 0 ]; do
|
|
117
|
+
case "$1" in
|
|
118
|
+
--port)
|
|
119
|
+
PORT=$2
|
|
120
|
+
shift 2
|
|
121
|
+
;;
|
|
122
|
+
--backend-url)
|
|
123
|
+
BACKEND_URL=$2
|
|
124
|
+
shift 2
|
|
125
|
+
;;
|
|
126
|
+
--model)
|
|
127
|
+
MODEL=$2
|
|
128
|
+
shift 2
|
|
129
|
+
;;
|
|
130
|
+
--reasoning-model)
|
|
131
|
+
REASONING_MODEL=$2
|
|
132
|
+
shift 2
|
|
133
|
+
;;
|
|
134
|
+
--ingress-api-key)
|
|
135
|
+
INGRESS_API_KEY=$2
|
|
136
|
+
shift 2
|
|
137
|
+
;;
|
|
138
|
+
--allow-origin)
|
|
139
|
+
join_allow_origin "$2"
|
|
140
|
+
shift 2
|
|
141
|
+
;;
|
|
142
|
+
--key-env)
|
|
143
|
+
API_KEY_ENV=$2
|
|
144
|
+
shift 2
|
|
145
|
+
;;
|
|
146
|
+
--api-key)
|
|
147
|
+
API_KEY=$2
|
|
148
|
+
shift 2
|
|
149
|
+
;;
|
|
150
|
+
--save-key)
|
|
151
|
+
SAVE_KEY=1
|
|
152
|
+
shift
|
|
153
|
+
;;
|
|
154
|
+
*)
|
|
155
|
+
echo "unknown option: $1" >&2
|
|
156
|
+
exit 1
|
|
157
|
+
;;
|
|
158
|
+
esac
|
|
159
|
+
done
|
|
160
|
+
|
|
161
|
+
if [ "$SAVE_KEY" -ne 1 ]; then
|
|
162
|
+
API_KEY=
|
|
163
|
+
fi
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
init_cmd() {
|
|
167
|
+
preset=${1:-}
|
|
168
|
+
[ -n "$preset" ] || {
|
|
169
|
+
echo "missing preset" >&2
|
|
170
|
+
usage
|
|
171
|
+
exit 1
|
|
172
|
+
}
|
|
173
|
+
shift
|
|
174
|
+
|
|
175
|
+
BACKEND_PROFILE=
|
|
176
|
+
BACKEND_URL=
|
|
177
|
+
MODEL=
|
|
178
|
+
REASONING_MODEL=
|
|
179
|
+
PORT=3000
|
|
180
|
+
INGRESS_API_KEY=
|
|
181
|
+
ALLOWED_ORIGINS=
|
|
182
|
+
API_KEY_ENV=
|
|
183
|
+
API_KEY=
|
|
184
|
+
BIN_PATH=$BIN_PATH_DEFAULT
|
|
185
|
+
|
|
186
|
+
case "$preset" in
|
|
187
|
+
chutes)
|
|
188
|
+
BACKEND_PROFILE=chutes
|
|
189
|
+
BACKEND_URL=https://llm.chutes.ai/v1
|
|
190
|
+
MODEL=Qwen/Qwen3-Coder-Next-TEE
|
|
191
|
+
API_KEY_ENV=CHUTES_API_KEY
|
|
192
|
+
;;
|
|
193
|
+
minimax)
|
|
194
|
+
BACKEND_PROFILE=openai-generic
|
|
195
|
+
BACKEND_URL=${MINIMAX_BASE_URL:-https://api.minimax.io/v1}
|
|
196
|
+
MODEL=${MINIMAX_MODEL:-MiniMax-M2.5}
|
|
197
|
+
API_KEY_ENV=MINIMAX_API_KEY
|
|
198
|
+
;;
|
|
199
|
+
openai)
|
|
200
|
+
BACKEND_PROFILE=openai-generic
|
|
201
|
+
;;
|
|
202
|
+
*)
|
|
203
|
+
echo "unsupported preset: $preset" >&2
|
|
204
|
+
exit 1
|
|
205
|
+
;;
|
|
206
|
+
esac
|
|
207
|
+
|
|
208
|
+
parse_common_init_args "$@"
|
|
209
|
+
|
|
210
|
+
if [ -z "$BACKEND_URL" ] || [ -z "$MODEL" ] || [ -z "$BACKEND_PROFILE" ]; then
|
|
211
|
+
echo "backend profile, backend url and model are required" >&2
|
|
212
|
+
exit 1
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
if [ -n "$API_KEY" ] && [ -n "$API_KEY_ENV" ]; then
|
|
216
|
+
API_KEY_ENV=
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
save_config
|
|
220
|
+
echo "saved config to $CONFIG_FILE"
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
set_cmd() {
|
|
224
|
+
ensure_config
|
|
225
|
+
load_config
|
|
226
|
+
field=${1:-}
|
|
227
|
+
[ -n "$field" ] || {
|
|
228
|
+
echo "missing field" >&2
|
|
229
|
+
exit 1
|
|
230
|
+
}
|
|
231
|
+
shift
|
|
232
|
+
|
|
233
|
+
case "$field" in
|
|
234
|
+
key-env)
|
|
235
|
+
API_KEY_ENV=${1:-}
|
|
236
|
+
[ -n "$API_KEY_ENV" ] || {
|
|
237
|
+
echo "missing env name" >&2
|
|
238
|
+
exit 1
|
|
239
|
+
}
|
|
240
|
+
API_KEY=
|
|
241
|
+
;;
|
|
242
|
+
key)
|
|
243
|
+
value=${1:-}
|
|
244
|
+
[ -n "$value" ] || {
|
|
245
|
+
echo "missing key value" >&2
|
|
246
|
+
exit 1
|
|
247
|
+
}
|
|
248
|
+
shift
|
|
249
|
+
save_flag=${1:-}
|
|
250
|
+
if [ "$save_flag" = "--save" ]; then
|
|
251
|
+
API_KEY=$value
|
|
252
|
+
API_KEY_ENV=
|
|
253
|
+
else
|
|
254
|
+
echo "use: scripts/anthmorphctl set key VALUE --save" >&2
|
|
255
|
+
exit 1
|
|
256
|
+
fi
|
|
257
|
+
;;
|
|
258
|
+
*)
|
|
259
|
+
echo "unsupported field: $field" >&2
|
|
260
|
+
exit 1
|
|
261
|
+
;;
|
|
262
|
+
esac
|
|
263
|
+
|
|
264
|
+
save_config
|
|
265
|
+
echo "updated $field"
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
unset_cmd() {
|
|
269
|
+
ensure_config
|
|
270
|
+
load_config
|
|
271
|
+
field=${1:-}
|
|
272
|
+
case "$field" in
|
|
273
|
+
key)
|
|
274
|
+
API_KEY=
|
|
275
|
+
API_KEY_ENV=
|
|
276
|
+
;;
|
|
277
|
+
*)
|
|
278
|
+
echo "unsupported unset field: $field" >&2
|
|
279
|
+
exit 1
|
|
280
|
+
;;
|
|
281
|
+
esac
|
|
282
|
+
save_config
|
|
283
|
+
echo "cleared $field"
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
start_cmd() {
|
|
287
|
+
load_config
|
|
288
|
+
|
|
289
|
+
if is_running; then
|
|
290
|
+
echo "already running (pid $(cat "$PID_FILE"))"
|
|
291
|
+
return 0
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
if health_on_port "$PORT" >/dev/null; then
|
|
295
|
+
echo "port $PORT is already serving an AnthMorph-compatible health endpoint" >&2
|
|
296
|
+
echo "use a different port with 'scripts/anthmorphctl init ... --port PORT'" >&2
|
|
297
|
+
exit 1
|
|
298
|
+
fi
|
|
299
|
+
|
|
300
|
+
if [ ! -x "$BIN_PATH" ]; then
|
|
301
|
+
(cd "$ROOT_DIR" && cargo build --release --quiet)
|
|
302
|
+
fi
|
|
303
|
+
|
|
304
|
+
api_key=$(resolve_api_key || true)
|
|
305
|
+
if [ -z "$api_key" ]; then
|
|
306
|
+
echo "missing API key: configure API_KEY_ENV or save an API key" >&2
|
|
307
|
+
exit 1
|
|
308
|
+
fi
|
|
309
|
+
|
|
310
|
+
set -- "$BIN_PATH" \
|
|
311
|
+
--port "$PORT" \
|
|
312
|
+
--backend-profile "$BACKEND_PROFILE" \
|
|
313
|
+
--backend-url "$BACKEND_URL" \
|
|
314
|
+
--model "$MODEL" \
|
|
315
|
+
--api-key "$api_key"
|
|
316
|
+
|
|
317
|
+
if [ -n "${REASONING_MODEL:-}" ]; then
|
|
318
|
+
set -- "$@" --reasoning-model "$REASONING_MODEL"
|
|
319
|
+
fi
|
|
320
|
+
if [ -n "${INGRESS_API_KEY:-}" ]; then
|
|
321
|
+
set -- "$@" --ingress-api-key "$INGRESS_API_KEY"
|
|
322
|
+
fi
|
|
323
|
+
|
|
324
|
+
old_ifs=${IFS:- }
|
|
325
|
+
IFS=,
|
|
326
|
+
for origin in ${ALLOWED_ORIGINS:-}; do
|
|
327
|
+
[ -n "$origin" ] || continue
|
|
328
|
+
set -- "$@" --allow-origin "$origin"
|
|
329
|
+
done
|
|
330
|
+
IFS=$old_ifs
|
|
331
|
+
|
|
332
|
+
nohup "$@" >>"$LOG_FILE" 2>&1 &
|
|
333
|
+
pid=$!
|
|
334
|
+
echo "$pid" > "$PID_FILE"
|
|
335
|
+
sleep 1
|
|
336
|
+
|
|
337
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
338
|
+
echo "started anthmorph (pid $pid)"
|
|
339
|
+
else
|
|
340
|
+
echo "failed to start; see $LOG_FILE" >&2
|
|
341
|
+
rm -f "$PID_FILE"
|
|
342
|
+
exit 1
|
|
343
|
+
fi
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
stop_cmd() {
|
|
347
|
+
if ! is_running; then
|
|
348
|
+
rm -f "$PID_FILE"
|
|
349
|
+
echo "not running"
|
|
350
|
+
return 0
|
|
351
|
+
fi
|
|
352
|
+
|
|
353
|
+
pid=$(cat "$PID_FILE")
|
|
354
|
+
kill "$pid" 2>/dev/null || true
|
|
355
|
+
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
|
356
|
+
if ! kill -0 "$pid" 2>/dev/null; then
|
|
357
|
+
rm -f "$PID_FILE"
|
|
358
|
+
echo "stopped anthmorph"
|
|
359
|
+
return 0
|
|
360
|
+
fi
|
|
361
|
+
sleep 1
|
|
362
|
+
done
|
|
363
|
+
kill -9 "$pid" 2>/dev/null || true
|
|
364
|
+
rm -f "$PID_FILE"
|
|
365
|
+
echo "killed anthmorph"
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
status_cmd() {
|
|
369
|
+
load_config
|
|
370
|
+
if is_running; then
|
|
371
|
+
pid=$(cat "$PID_FILE")
|
|
372
|
+
health=$(health_on_port "$PORT" || true)
|
|
373
|
+
echo "status: running"
|
|
374
|
+
echo "pid: $pid"
|
|
375
|
+
echo "health: ${health:-unreachable}"
|
|
376
|
+
else
|
|
377
|
+
health=$(health_on_port "$PORT" || true)
|
|
378
|
+
if [ -n "$health" ]; then
|
|
379
|
+
echo "status: external-process-on-port"
|
|
380
|
+
echo "health: $health"
|
|
381
|
+
else
|
|
382
|
+
echo "status: stopped"
|
|
383
|
+
fi
|
|
384
|
+
fi
|
|
385
|
+
echo "profile: ${BACKEND_PROFILE:-}"
|
|
386
|
+
echo "backend_url: ${BACKEND_URL:-}"
|
|
387
|
+
echo "model: ${MODEL:-}"
|
|
388
|
+
echo "port: ${PORT:-}"
|
|
389
|
+
if [ -n "${API_KEY_ENV:-}" ]; then
|
|
390
|
+
key_source=env:$API_KEY_ENV
|
|
391
|
+
elif [ -n "${API_KEY:-}" ]; then
|
|
392
|
+
key_source=saved
|
|
393
|
+
else
|
|
394
|
+
key_source=missing
|
|
395
|
+
fi
|
|
396
|
+
echo "key_source: $key_source"
|
|
397
|
+
echo "log_file: $LOG_FILE"
|
|
398
|
+
echo "config_file: $CONFIG_FILE"
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
logs_cmd() {
|
|
402
|
+
if [ -f "$LOG_FILE" ]; then
|
|
403
|
+
tail -n 80 "$LOG_FILE"
|
|
404
|
+
else
|
|
405
|
+
echo "log file not found: $LOG_FILE" >&2
|
|
406
|
+
exit 1
|
|
407
|
+
fi
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
print_config_cmd() {
|
|
411
|
+
ensure_config
|
|
412
|
+
sed '/^API_KEY=/s/=.*/=<hidden>/;/^INGRESS_API_KEY=/s/=.*/=<hidden>/' "$CONFIG_FILE"
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
cmd=${1:-help}
|
|
416
|
+
case "$cmd" in
|
|
417
|
+
init)
|
|
418
|
+
shift
|
|
419
|
+
init_cmd "$@"
|
|
420
|
+
;;
|
|
421
|
+
set)
|
|
422
|
+
shift
|
|
423
|
+
set_cmd "$@"
|
|
424
|
+
;;
|
|
425
|
+
unset)
|
|
426
|
+
shift
|
|
427
|
+
unset_cmd "$@"
|
|
428
|
+
;;
|
|
429
|
+
start)
|
|
430
|
+
start_cmd
|
|
431
|
+
;;
|
|
432
|
+
stop)
|
|
433
|
+
stop_cmd
|
|
434
|
+
;;
|
|
435
|
+
restart)
|
|
436
|
+
stop_cmd
|
|
437
|
+
start_cmd
|
|
438
|
+
;;
|
|
439
|
+
status)
|
|
440
|
+
status_cmd
|
|
441
|
+
;;
|
|
442
|
+
logs)
|
|
443
|
+
logs_cmd
|
|
444
|
+
;;
|
|
445
|
+
print-config)
|
|
446
|
+
print_config_cmd
|
|
447
|
+
;;
|
|
448
|
+
help|-h|--help)
|
|
449
|
+
usage
|
|
450
|
+
;;
|
|
451
|
+
*)
|
|
452
|
+
echo "unknown command: $cmd" >&2
|
|
453
|
+
usage >&2
|
|
454
|
+
exit 1
|
|
455
|
+
;;
|
|
456
|
+
esac
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const { spawnSync } = require("node:child_process");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
|
|
4
|
+
const root = path.resolve(__dirname, "..");
|
|
5
|
+
|
|
6
|
+
function hasCargo() {
|
|
7
|
+
const probe = spawnSync("cargo", ["--version"], { stdio: "ignore" });
|
|
8
|
+
return probe.status === 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (!hasCargo()) {
|
|
12
|
+
console.log("[anthmorph] cargo not found; skipping Rust build");
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const build = spawnSync("cargo", ["build", "--release", "--quiet"], {
|
|
17
|
+
cwd: root,
|
|
18
|
+
stdio: "inherit",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (build.status !== 0) {
|
|
22
|
+
process.exit(build.status || 1);
|
|
23
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
BACKEND=${1:?usage: ./scripts/smoke_test.sh <chutes|alibaba|minimax>}
|
|
5
|
+
PORT=${SMOKE_PORT:-3101}
|
|
6
|
+
PROMPT=${SMOKE_PROMPT:-Reply with exactly: anthmorph-smoke-ok}
|
|
7
|
+
LOG_FILE=${TMPDIR:-/tmp}/anthmorph-smoke-${BACKEND}-${PORT}.log
|
|
8
|
+
RESPONSE_FILE=${TMPDIR:-/tmp}/anthmorph-smoke-${BACKEND}-${PORT}.json
|
|
9
|
+
|
|
10
|
+
case "$BACKEND" in
|
|
11
|
+
chutes)
|
|
12
|
+
PROFILE=chutes
|
|
13
|
+
BACKEND_URL=${CHUTES_BASE_URL:-https://llm.chutes.ai/v1}
|
|
14
|
+
MODEL=${CHUTES_MODEL:-Qwen/Qwen3-Coder-Next-TEE}
|
|
15
|
+
API_KEY=${CHUTES_API_KEY:?CHUTES_API_KEY is required}
|
|
16
|
+
;;
|
|
17
|
+
alibaba)
|
|
18
|
+
PROFILE=openai-generic
|
|
19
|
+
BACKEND_URL=${ALIBABA_BASE_URL:-https://coding-intl.dashscope.aliyuncs.com/v1}
|
|
20
|
+
MODEL=${ALIBABA_MODEL:-qwen3-coder-plus}
|
|
21
|
+
API_KEY=${ALIBABA_CODE_API_KEY:?ALIBABA_CODE_API_KEY is required}
|
|
22
|
+
;;
|
|
23
|
+
minimax)
|
|
24
|
+
PROFILE=openai-generic
|
|
25
|
+
BACKEND_URL=${MINIMAX_BASE_URL:-https://api.minimax.io/v1}
|
|
26
|
+
MODEL=${MINIMAX_MODEL:-MiniMax-M2.5}
|
|
27
|
+
API_KEY=${MINIMAX_API_KEY:?MINIMAX_API_KEY is required}
|
|
28
|
+
;;
|
|
29
|
+
*)
|
|
30
|
+
echo "unsupported backend: $BACKEND" >&2
|
|
31
|
+
exit 2
|
|
32
|
+
;;
|
|
33
|
+
esac
|
|
34
|
+
|
|
35
|
+
cleanup() {
|
|
36
|
+
if [ -n "${SERVER_PID:-}" ] && kill -0 "$SERVER_PID" 2>/dev/null; then
|
|
37
|
+
kill "$SERVER_PID" 2>/dev/null || true
|
|
38
|
+
wait "$SERVER_PID" 2>/dev/null || true
|
|
39
|
+
fi
|
|
40
|
+
}
|
|
41
|
+
trap cleanup EXIT INT TERM
|
|
42
|
+
|
|
43
|
+
if [ ! -x ./target/debug/anthmorph ]; then
|
|
44
|
+
cargo build --quiet
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
./target/debug/anthmorph --port "$PORT" --backend-profile "$PROFILE" --backend-url "$BACKEND_URL" --model "$MODEL" --api-key "$API_KEY" >"$LOG_FILE" 2>&1 &
|
|
48
|
+
SERVER_PID=$!
|
|
49
|
+
|
|
50
|
+
READY=0
|
|
51
|
+
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30; do
|
|
52
|
+
if curl -fsS "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then
|
|
53
|
+
READY=1
|
|
54
|
+
break
|
|
55
|
+
fi
|
|
56
|
+
sleep 1
|
|
57
|
+
done
|
|
58
|
+
|
|
59
|
+
if [ "$READY" -ne 1 ]; then
|
|
60
|
+
echo "server did not become ready; log follows:" >&2
|
|
61
|
+
cat "$LOG_FILE" >&2
|
|
62
|
+
exit 1
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
PAYLOAD=$(cat <<EOF
|
|
66
|
+
{"model":"claude-sonnet-4","max_tokens":128,"messages":[{"role":"user","content":"$PROMPT"}]}
|
|
67
|
+
EOF
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
curl -fsS "http://127.0.0.1:$PORT/v1/messages" -H 'content-type: application/json' -d "$PAYLOAD" >"$RESPONSE_FILE"
|
|
71
|
+
|
|
72
|
+
cat "$RESPONSE_FILE"
|
package/src/config.rs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
use clap::ValueEnum;
|
|
2
|
+
use std::str::FromStr;
|
|
3
|
+
|
|
4
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
|
5
|
+
pub enum BackendProfile {
|
|
6
|
+
Chutes,
|
|
7
|
+
OpenaiGeneric,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
impl BackendProfile {
|
|
11
|
+
pub fn as_str(self) -> &'static str {
|
|
12
|
+
match self {
|
|
13
|
+
BackendProfile::Chutes => "chutes",
|
|
14
|
+
BackendProfile::OpenaiGeneric => "openai_generic",
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pub fn supports_top_k(self) -> bool {
|
|
19
|
+
matches!(self, BackendProfile::Chutes)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
pub fn supports_reasoning(self) -> bool {
|
|
23
|
+
matches!(self, BackendProfile::Chutes)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
impl FromStr for BackendProfile {
|
|
28
|
+
type Err = String;
|
|
29
|
+
|
|
30
|
+
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
|
31
|
+
match value.trim().to_ascii_lowercase().as_str() {
|
|
32
|
+
"chutes" => Ok(Self::Chutes),
|
|
33
|
+
"openai_generic" | "openai-generic" | "openai" | "generic" => Ok(Self::OpenaiGeneric),
|
|
34
|
+
other => Err(format!(
|
|
35
|
+
"unsupported backend profile '{other}', expected 'chutes' or 'openai_generic'"
|
|
36
|
+
)),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/error.rs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
use thiserror::Error;
|
|
2
|
+
|
|
3
|
+
#[derive(Error, Debug)]
|
|
4
|
+
pub enum ProxyError {
|
|
5
|
+
#[error("HTTP error: {0}")]
|
|
6
|
+
Http(#[from] reqwest::Error),
|
|
7
|
+
|
|
8
|
+
#[error("Upstream error: {0}")]
|
|
9
|
+
Upstream(String),
|
|
10
|
+
|
|
11
|
+
#[error("Transform error: {0}")]
|
|
12
|
+
Transform(String),
|
|
13
|
+
|
|
14
|
+
#[error("Serialization error: {0}")]
|
|
15
|
+
Serialization(#[from] serde_json::Error),
|
|
16
|
+
|
|
17
|
+
#[error("IO error: {0}")]
|
|
18
|
+
Io(#[from] std::io::Error),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
impl axum::response::IntoResponse for ProxyError {
|
|
22
|
+
fn into_response(self) -> axum::response::Response {
|
|
23
|
+
use axum::http::StatusCode;
|
|
24
|
+
|
|
25
|
+
let (status, error_type) = match &self {
|
|
26
|
+
ProxyError::Http(_) => (StatusCode::BAD_GATEWAY, "api_error"),
|
|
27
|
+
ProxyError::Upstream(msg) => {
|
|
28
|
+
if msg.contains("401") || msg.contains("403") {
|
|
29
|
+
(StatusCode::UNAUTHORIZED, "authentication_error")
|
|
30
|
+
} else if msg.contains("429") {
|
|
31
|
+
(StatusCode::TOO_MANY_REQUESTS, "rate_limit_error")
|
|
32
|
+
} else if msg.contains("404") {
|
|
33
|
+
(StatusCode::NOT_FOUND, "not_found_error")
|
|
34
|
+
} else {
|
|
35
|
+
(StatusCode::BAD_GATEWAY, "api_error")
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
ProxyError::Transform(_) => (StatusCode::BAD_REQUEST, "invalid_request_error"),
|
|
39
|
+
ProxyError::Serialization(_) => (StatusCode::BAD_REQUEST, "invalid_request_error"),
|
|
40
|
+
ProxyError::Io(_) => (StatusCode::INTERNAL_SERVER_ERROR, "api_error"),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
let payload = serde_json::json!({
|
|
44
|
+
"type": "error",
|
|
45
|
+
"error": {
|
|
46
|
+
"type": error_type,
|
|
47
|
+
"message": self.to_string()
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
(status, axum::Json(payload)).into_response()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
pub type ProxyResult<T> = Result<T, ProxyError>;
|