#!/usr/bin/env bash set -euo pipefail # OpenAI-compatible GLM Coding Plan full-cycle smoke. # # Deterministic, credential-free Edge -> Node -> loopback-provider full-cycle for # the built-in `profile: glm_coding` Coding Plan profile. A temporary Go fake # provider stands in for the Z.AI Coding Plan endpoint and only ever accepts the # Coding Plan paths: # GET /api/coding/paas/v4/models # POST /api/coding/paas/v4/chat/completions # It requires `Authorization: Bearer fake-glm-coding-token`, upstream model # `glm-5.1`, records non-streaming / streaming / tool-call variants, and fails # any General API `/api/paas/v4` request. No SOPS credential and no external host # are used. This script is a dedicated required diagnostic; it is intentionally # not part of `make test-e2e`. # # The provider is declared Provider-First (`type: openai_api`, `profile: # glm_coding`, root-only loopback `endpoint`), so profile normalization retains # the documented `/api/coding/paas/v4` base path while the loopback origin # replaces the upstream host. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # Repository-local ignored temp root under build/ so generated executables run # on an executable filesystem (a noexec /tmp breaks `go run`). GOTMPDIR is # relocated for the same reason; GOCACHE is inherited to keep builds warm. TMP_ROOT="${IOP_GLM_CODING_TMP_ROOT:-$REPO_ROOT/build/e2e-openai-glm-coding}" mkdir -p "$TMP_ROOT" TMP_DIR=$(mktemp -d "$TMP_ROOT/run.XXXXXX") mkdir -p "$TMP_DIR/go-tmp" export GOTMPDIR="$TMP_DIR/go-tmp" MODEL="glm-5.1" CLIENT_MODEL="glm-coding-smoke" PROVIDER_ID="glm-coding-smoke-provider" FAKE_AUTHORIZATION="Bearer fake-glm-coding-token" EDGE_PID="" NODE_PID="" GLM_PID="" EDGE_FD_OPEN=0 cleanup() { if [ "$EDGE_FD_OPEN" -eq 1 ]; then { echo "/exit" >&3; } 2>/dev/null || true exec 3>&- 2>/dev/null || true fi if [ -n "$EDGE_PID" ]; then wait "$EDGE_PID" 2>/dev/null || true; fi if [ -n "$NODE_PID" ]; then kill "$NODE_PID" 2>/dev/null || true; fi if [ -n "$GLM_PID" ]; then kill "$GLM_PID" 2>/dev/null || true; fi if [ "${IOP_GLM_CODING_KEEP_TMP:-0}" = "1" ]; then echo "[openai-glm-coding] keeping temp dir: $TMP_DIR" return fi rm -rf "$TMP_DIR" 2>/dev/null || true } trap cleanup EXIT PORT=$((31000 + RANDOM % 5000)) BOOTSTRAP_PORT=$((21000 + RANDOM % 5000)) OPENAI_PORT=$((36000 + RANDOM % 5000)) GLM_PORT=$((41000 + RANDOM % 5000)) EDGE_METRICS_PORT=$((46000 + RANDOM % 5000)) NODE_METRICS_PORT=$((51000 + RANDOM % 5000)) wait_port() { local host="$1" local port="$2" local label="$3" local deadline=$((SECONDS + 20)) while ! timeout 1 bash -c 'cat < /dev/null > /dev/tcp/"$1"/"$2"' _ "$host" "$port" 2>/dev/null; do if (( SECONDS >= deadline )); then echo "[openai-glm-coding] $label did not open on $host:$port" return 1 fi sleep 0.2 done } FAKE_GLM="$TMP_DIR/fake_glm_coding.go" cat > "$FAKE_GLM" <<'EOF' package main import ( "encoding/json" "fmt" "log" "net/http" "os" "sync/atomic" ) type chatRequest struct { Model string `json:"model"` Stream bool `json:"stream"` Tools []json.RawMessage `json:"tools"` } func main() { if len(os.Args) < 4 { log.Fatal("usage: fake_glm_coding ") } model := os.Args[2] authorization := os.Args[3] var codingModels atomic.Int64 var chatNonStream atomic.Int64 var chatStream atomic.Int64 var chatTool atomic.Int64 var generalAPIRequests atomic.Int64 var unexpectedRequests atomic.Int64 authorized := func(w http.ResponseWriter, r *http.Request) bool { if r.Header.Get("Authorization") != authorization { http.Error(w, "unexpected authorization", http.StatusUnauthorized) return false } return true } mux := http.NewServeMux() // Coding Plan models endpoint. mux.HandleFunc("/api/coding/paas/v4/models", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet || r.RequestURI != "/api/coding/paas/v4/models" { unexpectedRequests.Add(1) http.Error(w, "unexpected models request", http.StatusBadRequest) return } if !authorized(w, r) { return } codingModels.Add(1) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"object":"list","data":[{"id":"` + model + `"}]}`)) }) // Coding Plan chat endpoint. The only chat path this fake accepts. mux.HandleFunc("/api/coding/paas/v4/chat/completions", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost || r.RequestURI != "/api/coding/paas/v4/chat/completions" { unexpectedRequests.Add(1) http.Error(w, "unexpected chat request", http.StatusBadRequest) return } if !authorized(w, r) { return } var req chatRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if req.Model != model { http.Error(w, "unexpected model: "+req.Model, http.StatusBadRequest) return } // Tool-call variant: auto function-calling with the emit_marker tool. if len(req.Tools) > 0 { chatTool.Add(1) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"id":"chatcmpl-fake-tool","object":"chat.completion","model":"` + model + `","choices":[{"index":0,"message":{"role":"assistant","content":null,"tool_calls":[{"id":"call_fake","type":"function","function":{"name":"emit_marker","arguments":"{\"marker\":\"IOP_GLM_CODING_TOOL_OK\"}"}}]},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":6,"completion_tokens":3,"total_tokens":9}}`)) return } // Streaming variant. if req.Stream { chatStream.Add(1) w.Header().Set("Content-Type", "text/event-stream") flush := func() { if f, ok := w.(http.Flusher); ok { f.Flush() } } _, _ = w.Write([]byte("data: " + `{"choices":[{"delta":{"content":"IOP_GLM_CODING_"},"finish_reason":null}]}` + "\n\n")) flush() _, _ = w.Write([]byte("data: " + `{"choices":[{"delta":{"content":"STREAM_OK"},"finish_reason":"stop"}],"usage":{"prompt_tokens":6,"completion_tokens":2,"total_tokens":8}}` + "\n\n")) flush() _, _ = w.Write([]byte("data: [DONE]\n\n")) flush() return } // Non-streaming variant. chatNonStream.Add(1) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"id":"chatcmpl-fake","object":"chat.completion","model":"` + model + `","choices":[{"index":0,"message":{"role":"assistant","content":"IOP_GLM_CODING_NONSTREAM_OK"},"finish_reason":"stop"}],"usage":{"prompt_tokens":5,"completion_tokens":2,"total_tokens":7}}`)) }) // General API paths must never be reached by the glm_coding profile. mux.HandleFunc("/api/paas/v4/", func(w http.ResponseWriter, _ *http.Request) { generalAPIRequests.Add(1) http.Error(w, "general API path must not be used by glm_coding profile", http.StatusInternalServerError) }) // Assertion endpoint: succeeds only when the expected variants ran exactly once // over the Coding Plan path with no General API or unexpected upstream request. mux.HandleFunc("/assert", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "unexpected assertion method", http.StatusMethodNotAllowed) return } nonStream := chatNonStream.Load() stream := chatStream.Load() tool := chatTool.Load() general := generalAPIRequests.Load() unexpected := unexpectedRequests.Load() models := codingModels.Load() if nonStream != 1 || stream != 1 || tool != 1 || general != 0 || unexpected != 0 { http.Error(w, fmt.Sprintf("unexpected upstream profile: nonstream=%d stream=%d tool=%d general=%d unexpected=%d models=%d", nonStream, stream, tool, general, unexpected, models), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") _, _ = fmt.Fprintf(w, `{"nonstream":%d,"stream":%d,"tool":%d,"general":%d,"unexpected":%d,"models":%d}`, nonStream, stream, tool, general, unexpected, models) }) // Catch-all: any other upstream path is unexpected for this profile. mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { unexpectedRequests.Add(1) http.Error(w, "unexpected upstream path: "+r.URL.Path, http.StatusNotFound) }) log.Fatal(http.ListenAndServe(os.Args[1], mux)) } EOF go run "$FAKE_GLM" "127.0.0.1:$GLM_PORT" "$MODEL" "$FAKE_AUTHORIZATION" > "$TMP_DIR/fake_glm_coding.out" 2>&1 & GLM_PID=$! wait_port 127.0.0.1 "$GLM_PORT" "fake glm coding" # Edge config: Provider-First openai_api provider selecting the built-in # glm_coding profile with a root-only loopback endpoint and the fake Bearer # header. The client model glm-coding-smoke is rewritten to served model glm-5.1. EDGE_CONFIG="$TMP_DIR/edge.yaml" NODE_CONFIG="$TMP_DIR/node.yaml" cat > "$EDGE_CONFIG" < "$NODE_CONFIG" < "$EDGE_OUT" 2>&1 & EDGE_PID=$! exec 3> "$TMP_DIR/edge_fifo" EDGE_FD_OPEN=1 wait_port 127.0.0.1 "$PORT" "edge node transport" wait_port 127.0.0.1 "$OPENAI_PORT" "edge openai api" IOP_NODE_CONFIG="$NODE_CONFIG" "$REPO_ROOT/scripts/dev/node.sh" > "$NODE_OUT" 2>&1 & NODE_PID=$! deadline=$((SECONDS + 30)) while ! grep -q '\[node0-evt\] connected reason="registered"' "$EDGE_OUT"; do if (( SECONDS >= deadline )); then echo "[openai-glm-coding] node registration timed out" echo "=== EDGE OUTPUT ==="; cat "$EDGE_OUT" echo "=== NODE OUTPUT ==="; cat "$NODE_OUT" exit 1 fi sleep 0.2 done # Edge health. curl -fsS "http://127.0.0.1:$OPENAI_PORT/healthz" > /dev/null # /v1/models must surface the client-facing model id from the catalog. MODELS_OUT="$TMP_DIR/models.json" curl -fsS "http://127.0.0.1:$OPENAI_PORT/v1/models" > "$MODELS_OUT" grep -q "$CLIENT_MODEL" "$MODELS_OUT" # Non-streaming chat completion. CHAT_OUT="$TMP_DIR/chat.json" curl -fsS \ -H "Content-Type: application/json" \ -d "{\"model\":\"$CLIENT_MODEL\",\"max_tokens\":32,\"messages\":[{\"role\":\"user\",\"content\":\"Reply with exactly: smoke token\"}]}" \ "http://127.0.0.1:$OPENAI_PORT/v1/chat/completions" > "$CHAT_OUT" grep -q "IOP_GLM_CODING_NONSTREAM_OK" "$CHAT_OUT" grep -q '"total_tokens":7' "$CHAT_OUT" # Streaming chat completion: SSE content chunks and a terminating [DONE]. STREAM_OUT="$TMP_DIR/stream.txt" curl -fsS -N \ -H "Content-Type: application/json" \ -d "{\"model\":\"$CLIENT_MODEL\",\"stream\":true,\"max_tokens\":32,\"messages\":[{\"role\":\"user\",\"content\":\"Reply with exactly: stream token\"}]}" \ "http://127.0.0.1:$OPENAI_PORT/v1/chat/completions" > "$STREAM_OUT" grep -q '"content":"IOP_GLM_CODING_"' "$STREAM_OUT" grep -q 'data: \[DONE\]' "$STREAM_OUT" # Auto function-calling chat completion: the emit_marker tool must be forwarded # and its tool_call must survive the round trip. TOOL_OUT="$TMP_DIR/tool.json" curl -fsS \ -H "Content-Type: application/json" \ -d "{\"model\":\"$CLIENT_MODEL\",\"max_tokens\":32,\"tool_choice\":\"auto\",\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"emit_marker\",\"description\":\"emit a marker\",\"parameters\":{\"type\":\"object\",\"properties\":{\"marker\":{\"type\":\"string\"}},\"required\":[\"marker\"]}}}],\"messages\":[{\"role\":\"user\",\"content\":\"Call emit_marker with marker set to done\"}]}" \ "http://127.0.0.1:$OPENAI_PORT/v1/chat/completions" > "$TOOL_OUT" grep -q '"tool_calls"' "$TOOL_OUT" grep -q '"emit_marker"' "$TOOL_OUT" # The fake provider must have observed exactly the Coding Plan path, the fake # Bearer header, the served model, and each variant once, with no General API # path and no unexpected upstream request. FAKE_ASSERT_OUT="$TMP_DIR/fake_assert.json" curl -fsS "http://127.0.0.1:$GLM_PORT/assert" > "$FAKE_ASSERT_OUT" grep -q '"nonstream":1' "$FAKE_ASSERT_OUT" grep -q '"stream":1' "$FAKE_ASSERT_OUT" grep -q '"tool":1' "$FAKE_ASSERT_OUT" grep -q '"general":0' "$FAKE_ASSERT_OUT" grep -q '"unexpected":0' "$FAKE_ASSERT_OUT" if grep -i -E "node reported error|error run_id=|\[[^]]+-evt\] error|panic:" "$EDGE_OUT" "$NODE_OUT" >/dev/null; then echo "[openai-glm-coding] detected failure marker" echo "=== EDGE OUTPUT ==="; cat "$EDGE_OUT" echo "=== NODE OUTPUT ==="; cat "$NODE_OUT" exit 1 fi echo "[openai-glm-coding] glm_coding Edge-Node-provider full-cycle PASSED."