-
Notifications
You must be signed in to change notification settings - Fork 71
feat: add SGLang conformance recordings and CI replay #267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
52ad07d
feat: add SGLang provider conformance recordings and CI replay
franciscojavierarceo 6a64186
fix: address PR review feedback
franciscojavierarceo 3f80ff9
Merge branch 'main' into codex/sglang-provider-conformance
franciscojavierarceo e9a2aba
Merge branch 'main' into codex/sglang-provider-conformance
franciscojavierarceo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
76 changes: 76 additions & 0 deletions
76
crates/agentic-server-core/tests/cassettes/prepare_sglang_recordings.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| #!/usr/bin/env python3 | ||
| """Attach provenance and sanitize recorder-produced SGLang cassettes in staging.""" | ||
|
|
||
| import json | ||
| import sys | ||
| from pathlib import Path | ||
| from urllib.request import urlopen | ||
|
|
||
| import yaml | ||
|
|
||
|
|
||
| def prepare(root: Path, version: str, model: str, upstream: str) -> None: | ||
| with urlopen(upstream.rstrip("/") + "/server_info", timeout=15) as response: | ||
| info = json.load(response) | ||
| if info["version"] != version or info["model_path"] != model: | ||
| raise ValueError("Running SGLang version/model does not match the requested recording profile") | ||
| launch = { | ||
| key: info[key] | ||
| for key in ( | ||
| "revision", "reasoning_parser", "tool_call_parser", "context_length", | ||
| "mem_fraction_static", "dtype", "quantization", "tp_size", "random_seed", | ||
| ) | ||
| } | ||
| for path in sorted(root.glob("*.yaml")): | ||
| cassette = yaml.safe_load(path.read_text()) | ||
| identifiers = {} | ||
|
|
||
| def sanitize(value, key=""): | ||
| if isinstance(value, dict): | ||
| return {name: sanitize(item, name) for name, item in value.items()} | ||
| if isinstance(value, list): | ||
| return [sanitize(item, key) for item in value] | ||
| if key in {"created_at", "completed_at"} and isinstance(value, (float, int)): | ||
| # Keep provider wire number types while removing wall-clock time. | ||
| return 0.0 if isinstance(value, float) else 0 | ||
| if key in {"id", "item_id", "call_id", "previous_response_id"} and isinstance(value, str): | ||
| if value not in identifiers: | ||
| identifiers[value] = f"recorded_{len(identifiers) + 1}" | ||
| return identifiers[value] | ||
| return value | ||
|
|
||
| for turn in cassette["turns"]: | ||
| turn["request"]["headers"] = {"content-type": "application/json"} | ||
| turn["request"]["query_params"] = {} | ||
| turn["request"]["body"] = sanitize(turn["request"]["body"]) | ||
| response = turn["response"] | ||
| if response.get("body") is not None: | ||
| response["body"] = sanitize(response["body"]) | ||
| else: | ||
| sanitized = [] | ||
| for raw in response["sse"]: | ||
| lines = [] | ||
| for line in raw.splitlines(keepends=True): | ||
| if line.startswith("data:") and line[5:].strip() != "[DONE]": | ||
| ending = "\n" if line.endswith("\n") else "" | ||
| line = "data: " + json.dumps(sanitize(json.loads(line[5:])), separators=(",", ":")) + ending | ||
| lines.append(line) | ||
| sanitized.append("".join(lines)) | ||
| response["sse"] = sanitized | ||
| cassette["provider"] = { | ||
| "name": "sglang", | ||
| "version": version, | ||
| "model": model, | ||
| "transport": "http-sse" if cassette["turns"][0]["request"]["body"]["stream"] else "http-json", | ||
| "launch": launch, | ||
| "capabilities_exercised": ( | ||
| ["text", "gateway_stateful_continuation"] | ||
| if "stateful" in path.name else ["client_function_call"] | ||
| ), | ||
| "unverified": ["parallel_function_calls", "structured_text", "upstream_websocket", "reasoning_summary"], | ||
| } | ||
| path.write_text(yaml.safe_dump(cassette, sort_keys=False, allow_unicode=True, width=10**9)) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| prepare(Path(sys.argv[1]), sys.argv[2], sys.argv[3], sys.argv[4]) |
131 changes: 131 additions & 0 deletions
131
crates/agentic-server-core/tests/cassettes/record_sglang_cassettes.sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| #!/usr/bin/env bash | ||
| # Record real SGLang traffic using the shared recorder and Dynamo scenario definitions. | ||
| # See docs/guides/sglang-upstream.md for the pinned launch configuration. | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| DEST_DIR="$SCRIPTS_DIR/sglang" | ||
| STAGING_ROOT="$(mktemp -d -t agentic-sglang-recording.XXXXXXXX)" | ||
| BASE_DIR="$STAGING_ROOT/sglang" | ||
| TOOLS_FILE="$SCRIPTS_DIR/tool_calls/tools.json" | ||
| SGLANG_URL="${SGLANG_URL:-http://127.0.0.1:30000}" | ||
| MODEL="${MODEL:-Qwen/Qwen3-8B}" | ||
| MODEL_SLUG="$(echo "$MODEL" | tr '/: ' '---')" | ||
| PYTHON="${PYTHON:-python}" | ||
| SGLANG_VERSION="${SGLANG_VERSION:-0.5.18}" | ||
|
|
||
| green() { printf '\033[32m%s\033[0m\n' "$*"; } | ||
| bold() { printf '\033[1m%s\033[0m\n' "$*"; } | ||
|
|
||
| mkdir -p "$BASE_DIR" | ||
|
|
||
| bold "SGLang URL: $SGLANG_URL" | ||
| bold "Model: $MODEL" | ||
| echo | ||
|
|
||
| # record NAME STREAM_FLAG PROMPT [extra recorder args...] | ||
| record() { | ||
| local name="$1" stream_flag="$2" prompt="$3"; shift 3 | ||
| local suffix; [[ -n "$stream_flag" ]] && suffix=nonstreaming || suffix=streaming | ||
| bold "── $name ($suffix) ──" | ||
| record_into "$BASE_DIR/${name}-${MODEL_SLUG}-${suffix}.yaml" "$stream_flag" "$prompt" "$@" | ||
| green "✓ $name ($suffix) done." | ||
| } | ||
|
|
||
| # append_turn NAME STREAM_FLAG PROMPT [extra recorder args...] | ||
| # The recorder truncates its output file, so extra turns are recorded separately and merged. | ||
| append_turn() { | ||
| local name="$1" stream_flag="$2" prompt="$3"; shift 3 | ||
| local suffix; [[ -n "$stream_flag" ]] && suffix=nonstreaming || suffix=streaming | ||
| local target="$BASE_DIR/${name}-${MODEL_SLUG}-${suffix}.yaml" | ||
| local extra="${target%.yaml}.next-turn.yaml" | ||
| bold "── $name ($suffix), next turn ──" | ||
| record_into "$extra" "$stream_flag" "$prompt" "$@" | ||
| $PYTHON - "$target" "$extra" <<'PY' | ||
| import sys, yaml | ||
| target, extra = sys.argv[1], sys.argv[2] | ||
| merged = yaml.safe_load(open(target)) | ||
| for turn in yaml.safe_load(open(extra))["turns"]: | ||
| turn["filename"] = f"t{len(merged['turns']) + 1}" | ||
| merged["turns"].append(turn) | ||
| yaml.safe_dump(merged, open(target, "w"), sort_keys=False, allow_unicode=True, width=10**9) | ||
| PY | ||
| rm -f "$extra" | ||
| green "✓ $name ($suffix) turn appended." | ||
| } | ||
|
|
||
| record_into() { | ||
| local output="$1" stream_flag="$2" prompt="$3"; shift 3 | ||
| # shellcheck disable=SC2086 | ||
| printf '%s\n' "$prompt" | $PYTHON "$SCRIPTS_DIR/record_cassette.py" \ | ||
| --mode responses \ | ||
| --turns 1 \ | ||
| --model "$MODEL" \ | ||
| --vllm "$SGLANG_URL" \ | ||
| --max-output-tokens 2048 \ | ||
| $stream_flag \ | ||
| "$@" \ | ||
| --output "$output" | ||
| } | ||
|
|
||
| # hydrated_turn2_input CASSETTE OUT_JSON | ||
| # Writes the item history the gateway sends for turn 2: the user prompt, the assistant message exactly as | ||
| # recorded in turn 1 (same id and text), and the follow-up user prompt. | ||
| hydrated_turn2_input() { | ||
| $PYTHON - "$1" "$2" "$TURN1_PROMPT" "$TURN2_PROMPT" <<'PY' | ||
| import json, sys, yaml | ||
| cassette, out, turn1, turn2 = sys.argv[1:5] | ||
| response = yaml.safe_load(open(cassette))["turns"][0]["response"] | ||
| if response.get("body"): | ||
| completed = response["body"] | ||
| else: | ||
| completed = next( | ||
| json.loads(line[len("data: "):])["response"] | ||
| for raw in response["sse"] | ||
| for line in raw.splitlines() | ||
| if line.startswith("data: ") and json.loads(line[len("data: "):]).get("type") == "response.completed" | ||
| ) | ||
| history = [{"type": "message", "role": "user", "content": turn1}] | ||
| for item in completed["output"]: | ||
| if item["type"] == "reasoning": | ||
| history.append(item) | ||
| elif item["type"] == "message": | ||
| history.append({ | ||
| "type": "message", | ||
| "id": item["id"], | ||
| "role": "assistant", | ||
| "status": "completed", | ||
| "content": [{"type": "output_text", "text": part["text"]} for part in item["content"]], | ||
| }) | ||
| else: | ||
| raise ValueError(f"Unexpected output item in text scenario: {item['type']}") | ||
| history.append({"type": "message", "role": "user", "content": turn2}) | ||
| json.dump(history, open(out, "w"), indent=2) | ||
| PY | ||
| } | ||
|
|
||
| TURN1_PROMPT="Remember the word APPLE. Just say: OK" | ||
| TURN2_PROMPT="What word did I ask you to remember? Reply with just the word." | ||
|
|
||
| for stream_flag in --no-stream ""; do | ||
| [[ -n "$stream_flag" ]] && suffix=nonstreaming || suffix=streaming | ||
| record sglang-stateful "$stream_flag" "$TURN1_PROMPT" | ||
| turn2_input="$(mktemp --suffix=.json)" | ||
| hydrated_turn2_input "$BASE_DIR/sglang-stateful-${MODEL_SLUG}-${suffix}.yaml" "$turn2_input" | ||
| append_turn sglang-stateful "$stream_flag" "" --input-file "$turn2_input" | ||
| rm -f "$turn2_input" | ||
| record sglang-tool-call-auto "$stream_flag" "What is the current NVIDIA stock price? Use the tool." \ | ||
| --tools "$TOOLS_FILE" --tool-choice auto | ||
| done | ||
|
|
||
| echo | ||
| "$PYTHON" "$SCRIPTS_DIR/prepare_sglang_recordings.py" "$BASE_DIR" "$SGLANG_VERSION" "$MODEL" "$SGLANG_URL" | ||
| "$PYTHON" "$SCRIPTS_DIR/../../../../scripts/validate-cassettes.py" "$BASE_DIR" | ||
| ( | ||
| cd "$SCRIPTS_DIR/../../../.." | ||
| PROVIDER_CASSETTE_ROOT="$STAGING_ROOT" cargo test -p agentic-server-core --test sglang_cassette_test | ||
| ) | ||
| mkdir -p "$DEST_DIR" | ||
| cp "$BASE_DIR"/*.yaml "$DEST_DIR/" | ||
| green "All SGLang cassettes validated and published -> $DEST_DIR (staging: $STAGING_ROOT)" |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.