Text Generation
Transformers
Safetensors
mistral3
image-text-to-text
decision-model
typed-decisions
jev
jevbench
calibration
decode-free
multilingual
vision-language
conversational
Instructions to use StandardThinking/StandardOne-3B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use StandardThinking/StandardOne-3B with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="StandardThinking/StandardOne-3B") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] pipe(text=messages)# pip install -U transformers accelerate # Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("StandardThinking/StandardOne-3B") model = AutoModelForMultimodalLM.from_pretrained("StandardThinking/StandardOne-3B", device_map="auto") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] inputs = processor.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=256) print(processor.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use StandardThinking/StandardOne-3B with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "StandardThinking/StandardOne-3B" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "StandardThinking/StandardOne-3B", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/StandardThinking/StandardOne-3B
- SGLang
How to use StandardThinking/StandardOne-3B with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "StandardThinking/StandardOne-3B" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "StandardThinking/StandardOne-3B", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "StandardThinking/StandardOne-3B" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "StandardThinking/StandardOne-3B", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Docker Model Runner
How to use StandardThinking/StandardOne-3B with Docker Model Runner:
docker model run hf.co/StandardThinking/StandardOne-3B
Add serving code
Browse files- SHA256SUMS +7 -7
- server/README.md +31 -1
- server/jev_adapter/__main__.py +23 -0
- server/jev_adapter/server.py +2 -0
- server/jev_adapter/service.py +50 -1
- server/jev_adapter/sglang.py +3 -1
- server/tests/test_service.py +45 -1
- server/tests/test_sglang.py +30 -0
SHA256SUMS
CHANGED
|
@@ -34,7 +34,7 @@ f9913e0d49fc053df656058d13af2523b6263f9396efad7a6fa06e7a121e1acf release-manife
|
|
| 34 |
eef2a88a000d2ec294379755770c147327cb79d3d7278c711899bd36797d7a02 server/.gitignore
|
| 35 |
1495e1e757ef4d0925a2350563cf5754bb23c51701a8ec4fb3c5cdcbedae6747 server/LICENSE
|
| 36 |
0f1f023b916d38d7c12af3a71e09f5e82b819a7e156e4992118bb160ee63ede1 server/NOTICE
|
| 37 |
-
|
| 38 |
88e4dea8cc92f467a03bdd45a5fb968e402517116b09e24edcea85bb15f121e5 server/benchmarks/DECISION_BENCHMARK_SELECTION.md
|
| 39 |
7fea6b9dc44e34ce448f1d80a9f2d273b76ab9b0ac1859c032fa4faa4f458501 server/benchmarks/METRIC_VALIDATION.md
|
| 40 |
bb88f12ef294680e389147d781bf1f8680ea0a5ca9ed554730d8ea29f1c25eb9 server/benchmarks/PUBLIC_DATASETS.md
|
|
@@ -56,7 +56,7 @@ fee797d1a2ff128a5d61dd436e8252d63bc726b8ce4ba72641a1ffd9196135a6 server/benchma
|
|
| 56 |
2544280dd3e669985bf57dfc61c2212bcf16d27230a79cea76f9f4e822476429 server/examples/request.json
|
| 57 |
7a14fe0f5bf73aca7a4930423c310c0a3b1f9b546d07abc8d3fd5d9f38c9a5d7 server/examples/smoke.py
|
| 58 |
e589f268969c87f82ddd2c81c0432c6335c08a9f9f2537ce68ddd5708f1e6f9e server/jev_adapter/__init__.py
|
| 59 |
-
|
| 60 |
e2f84bb7867934533f8f2a87034cad76af695ca1bbdbf096c545fe60b790c6fa server/jev_adapter/backend.py
|
| 61 |
2cd67e71c1a601fc9333c266d909197031b7c0dc746b9a2d5475a5fb0c085e95 server/jev_adapter/benchmarks/__init__.py
|
| 62 |
44104bc2d6431c05345cd697dd2c377cc6ba9fd57f2177df7b548490cf5f53fc server/jev_adapter/benchmarks/compare.py
|
|
@@ -67,9 +67,9 @@ a66e6ec289b4c32f79ac990261650870be6b923e47753b9c89ff9e58fe2f982f server/jev_ada
|
|
| 67 |
0fef99cd01164c5548de530c9ab93255e2a95d17757c12e8e3d23016afd1a03c server/jev_adapter/benchmarks/run.py
|
| 68 |
86e113a3ee9d15d9be93ba5e964d5f5d51171619e7856cc7dde812c6c90f4a37 server/jev_adapter/native_tokenizer.py
|
| 69 |
ad7dee34b9c15e83ce66b3b8b07266e0b8ce81da7c8ba692a8293dbf078d0e44 server/jev_adapter/protocol.py
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
159eb400efb830c762a36f81e51ba79888570284ffa70326301adfeee50b5bd3 server/pyproject.toml
|
| 74 |
f5bf6e9b916ff0d24a2551b4f9f24ed353e7343f5be343b141d38c4c04bd1101 server/tests/test_benchmark_data.py
|
| 75 |
c197a90d57c7516eb615f7f0626f4c3e95b2fd0773d74e8f32601251804fca66 server/tests/test_benchmark_matrix.py
|
|
@@ -80,8 +80,8 @@ c492b41c07050d9b76c6c9d7d21bbfcdac6ec137346cf995df3f624cb1e345c8 server/tests/t
|
|
| 80 |
ade6b76bacf23ae244d9790312f297a36557d6cd8075b81ce36e928359b742da server/tests/test_main.py
|
| 81 |
83084f098020374a4abfa3d4402e74e357d45170ca840843e58dba13162e9986 server/tests/test_native_tokenizer.py
|
| 82 |
0114da8966dafa951308ed58563053918b2b2f7b3393dd483c7183e3374b444d server/tests/test_protocol.py
|
| 83 |
-
|
| 84 |
-
|
| 85 |
0a5c981e8c5c6f8886ee007a6d4543a0be6b221cb9ca32a8709384a4c6fc8cbb special_tokens_map.json
|
| 86 |
600bb27946565481ecf51ba8aee252e49b9a68507866080ac9c30185bb312843 tekken.json
|
| 87 |
d5f6046775b112f0e2d456ee9dba450684ab964fe5c4e231599bdc6773028135 tokenizer.json
|
|
|
|
| 34 |
eef2a88a000d2ec294379755770c147327cb79d3d7278c711899bd36797d7a02 server/.gitignore
|
| 35 |
1495e1e757ef4d0925a2350563cf5754bb23c51701a8ec4fb3c5cdcbedae6747 server/LICENSE
|
| 36 |
0f1f023b916d38d7c12af3a71e09f5e82b819a7e156e4992118bb160ee63ede1 server/NOTICE
|
| 37 |
+
8db88b70be27a581c975b872abc0ba7eaafa092cc38db4debd3970bc83fb013a server/README.md
|
| 38 |
88e4dea8cc92f467a03bdd45a5fb968e402517116b09e24edcea85bb15f121e5 server/benchmarks/DECISION_BENCHMARK_SELECTION.md
|
| 39 |
7fea6b9dc44e34ce448f1d80a9f2d273b76ab9b0ac1859c032fa4faa4f458501 server/benchmarks/METRIC_VALIDATION.md
|
| 40 |
bb88f12ef294680e389147d781bf1f8680ea0a5ca9ed554730d8ea29f1c25eb9 server/benchmarks/PUBLIC_DATASETS.md
|
|
|
|
| 56 |
2544280dd3e669985bf57dfc61c2212bcf16d27230a79cea76f9f4e822476429 server/examples/request.json
|
| 57 |
7a14fe0f5bf73aca7a4930423c310c0a3b1f9b546d07abc8d3fd5d9f38c9a5d7 server/examples/smoke.py
|
| 58 |
e589f268969c87f82ddd2c81c0432c6335c08a9f9f2537ce68ddd5708f1e6f9e server/jev_adapter/__init__.py
|
| 59 |
+
bf3d7a38ac294b23bf0d1a6b32a66fffb5054affc7d8958a821d9aea4f3ef807 server/jev_adapter/__main__.py
|
| 60 |
e2f84bb7867934533f8f2a87034cad76af695ca1bbdbf096c545fe60b790c6fa server/jev_adapter/backend.py
|
| 61 |
2cd67e71c1a601fc9333c266d909197031b7c0dc746b9a2d5475a5fb0c085e95 server/jev_adapter/benchmarks/__init__.py
|
| 62 |
44104bc2d6431c05345cd697dd2c377cc6ba9fd57f2177df7b548490cf5f53fc server/jev_adapter/benchmarks/compare.py
|
|
|
|
| 67 |
0fef99cd01164c5548de530c9ab93255e2a95d17757c12e8e3d23016afd1a03c server/jev_adapter/benchmarks/run.py
|
| 68 |
86e113a3ee9d15d9be93ba5e964d5f5d51171619e7856cc7dde812c6c90f4a37 server/jev_adapter/native_tokenizer.py
|
| 69 |
ad7dee34b9c15e83ce66b3b8b07266e0b8ce81da7c8ba692a8293dbf078d0e44 server/jev_adapter/protocol.py
|
| 70 |
+
bf9c1588e8069ef2c341151c24f08b29f41d9bc57cb16f1866467467b50d8edc server/jev_adapter/server.py
|
| 71 |
+
332068ebc21f19b6eee6c6d8fd24e6821a4fbd5def6251f5a94beae613419a0a server/jev_adapter/service.py
|
| 72 |
+
2e45894cfb8fa204c7278ade4a123cda356de894ddf6fbb60b66e93dfea576c6 server/jev_adapter/sglang.py
|
| 73 |
159eb400efb830c762a36f81e51ba79888570284ffa70326301adfeee50b5bd3 server/pyproject.toml
|
| 74 |
f5bf6e9b916ff0d24a2551b4f9f24ed353e7343f5be343b141d38c4c04bd1101 server/tests/test_benchmark_data.py
|
| 75 |
c197a90d57c7516eb615f7f0626f4c3e95b2fd0773d74e8f32601251804fca66 server/tests/test_benchmark_matrix.py
|
|
|
|
| 80 |
ade6b76bacf23ae244d9790312f297a36557d6cd8075b81ce36e928359b742da server/tests/test_main.py
|
| 81 |
83084f098020374a4abfa3d4402e74e357d45170ca840843e58dba13162e9986 server/tests/test_native_tokenizer.py
|
| 82 |
0114da8966dafa951308ed58563053918b2b2f7b3393dd483c7183e3374b444d server/tests/test_protocol.py
|
| 83 |
+
617f08c82448932cf13320166c4df66a1a85b5056a354aa8e9f748ce9b6f57d3 server/tests/test_service.py
|
| 84 |
+
340e91fb9a6c870990187386da8d165fafcbcb5f08559056fd9da4e4e78b681f server/tests/test_sglang.py
|
| 85 |
0a5c981e8c5c6f8886ee007a6d4543a0be6b221cb9ca32a8709384a4c6fc8cbb special_tokens_map.json
|
| 86 |
600bb27946565481ecf51ba8aee252e49b9a68507866080ac9c30185bb312843 tekken.json
|
| 87 |
d5f6046775b112f0e2d456ee9dba450684ab964fe5c4e231599bdc6773028135 tokenizer.json
|
server/README.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# Jev adapter
|
| 2 |
|
| 3 |
기본 추론 엔진을 별도 프로세스로 실행하고, 그 앞에서 `POST /v1/systemone` 응답을 만드는 독립 Python 서비스입니다. 모델 학습, 엔진 fork, SGLang Python 패키지, CUDA, 모델 가중치가 어댑터에 필요하지 않습니다. 현재 구현한 백엔드는 **SGLang HTTP API**입니다.
|
|
@@ -76,7 +106,7 @@ ablation에서 이 문구 차이만으로 jevbench-hard 정확도가 약 +6점
|
|
| 76 |
Ministral처럼 서빙 토크나이저가 `mistral-common` 백엔드로 해석되는 경우
|
| 77 |
(`native_tokenizer.py` 문서 참고) 이 백엔드는 기본 시스템 프롬프트를 자동으로
|
| 78 |
주입하지 않기 때문입니다 — 직접 추출한 텍스트가 없으면 시스템 메시지 없이 문구만
|
| 79 |
-
`native`로 서빙합니다(시작 시 경고 로그). `
|
| 80 |
문서 파일은 실제 chat template이 렌더링하는 기본 텍스트와 다를 수 있어(확인된 사례:
|
| 81 |
문장 하나가 중복/누락) 신뢰하지 않고, 항상 `apply_chat_template`을 직접 렌더링해서
|
| 82 |
추출합니다.
|
|
|
|
| 1 |
+
## Serving Standard One 3B with this code
|
| 2 |
+
|
| 3 |
+
This directory is `jev-adapter` (Apache-2.0, see `LICENSE` and `NOTICE`): a small HTTP server that turns the SGLang engine into a
|
| 4 |
+
`POST /v1/systemone` decision endpoint. Install it into its own virtual environment and start it in front of a stock SGLang 0.5.20
|
| 5 |
+
engine that serves this repository:
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
pip install -e './server[native-tokenizer]'
|
| 9 |
+
|
| 10 |
+
# terminal 1: engine
|
| 11 |
+
CUDA_VISIBLE_DEVICES=0 SGLANG_VLM_CACHE_SIZE_MB=0 python -m sglang.launch_server \
|
| 12 |
+
--model-path . --served-model-name standard-one-3b \
|
| 13 |
+
--host 127.0.0.1 --port 30000 --tp-size 1 --model-impl sglang --dtype bfloat16 \
|
| 14 |
+
--context-length 8192 --max-running-requests 32 --mem-fraction-static 0.8 \
|
| 15 |
+
--chunked-prefill-size -1 --disable-radix-cache --mm-preprocess-cache-size-mb 0 \
|
| 16 |
+
--model-config-parser hf --load-format safetensors
|
| 17 |
+
|
| 18 |
+
# terminal 2: adapter (no system prompt; temperatures: see the model card)
|
| 19 |
+
jev-adapter --engine-url http://127.0.0.1:30000 --model standard-one-3b --alias jev-latest \
|
| 20 |
+
--host 0.0.0.0 --port 30120 --max-concurrency 1 \
|
| 21 |
+
--tokenizer-model mistralai/Ministral-3-3B-Instruct-2512-BF16 \
|
| 22 |
+
--tokenizer-revision b6d637bef2393152b3da2b2fde72eecdee30557e \
|
| 23 |
+
--prompt-wording native --native-system-prompt none \
|
| 24 |
+
--default-temperature <T> --temperature-by-type choice=<T>,noul=<T>,score=<T>
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
`--temperature-by-type` sets one default temperature per answer type (`choice`, `noul`, `score`); a request's own
|
| 28 |
+
`options.temperature` still wins. Let only this adapter talk to the engine port. The recommended values and the full
|
| 29 |
+
request format are in the model card (`../README.md`).
|
| 30 |
+
|
| 31 |
# Jev adapter
|
| 32 |
|
| 33 |
기본 추론 엔진을 별도 프로세스로 실행하고, 그 앞에서 `POST /v1/systemone` 응답을 만드는 독립 Python 서비스입니다. 모델 학습, 엔진 fork, SGLang Python 패키지, CUDA, 모델 가중치가 어댑터에 필요하지 않습니다. 현재 구현한 백엔드는 **SGLang HTTP API**입니다.
|
|
|
|
| 106 |
Ministral처럼 서빙 토크나이저가 `mistral-common` 백엔드로 해석되는 경우
|
| 107 |
(`native_tokenizer.py` 문서 참고) 이 백엔드는 기본 시스템 프롬프트를 자동으로
|
| 108 |
주입하지 않기 때문입니다 — 직접 추출한 텍스트가 없으면 시스템 메시지 없이 문구만
|
| 109 |
+
`native`로 서빙합니다(시작 시 경고 로그). `SYSTEM_PROMPT.txt` 같은 정적
|
| 110 |
문서 파일은 실제 chat template이 렌더링하는 기본 텍스트와 다를 수 있어(확인된 사례:
|
| 111 |
문장 하나가 중복/누락) 신뢰하지 않고, 항상 `apply_chat_template`을 직접 렌더링해서
|
| 112 |
추출합니다.
|
server/jev_adapter/__main__.py
CHANGED
|
@@ -19,6 +19,20 @@ def positive_temperature(text: str) -> float:
|
|
| 19 |
return value
|
| 20 |
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
def main():
|
| 23 |
parser = argparse.ArgumentParser(description=__doc__)
|
| 24 |
parser.add_argument("--engine-url", default="http://127.0.0.1:30000")
|
|
@@ -40,6 +54,14 @@ def main():
|
|
| 40 |
"(label logits are divided by T before softmax; an explicit request "
|
| 41 |
"value wins). Env: JEV_DEFAULT_TEMPERATURE. Default 1.0.",
|
| 42 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
parser.add_argument(
|
| 44 |
"--prompt-wording",
|
| 45 |
choices=("served", "native"),
|
|
@@ -136,6 +158,7 @@ def main():
|
|
| 136 |
max_concurrency=args.max_concurrency,
|
| 137 |
alias=args.alias,
|
| 138 |
default_temperature=args.default_temperature,
|
|
|
|
| 139 |
prompt_wording=args.prompt_wording,
|
| 140 |
)
|
| 141 |
uvicorn.run(app, host=args.host, port=args.port)
|
|
|
|
| 19 |
return value
|
| 20 |
|
| 21 |
|
| 22 |
+
def temperature_map(text: str) -> dict[str, float]:
|
| 23 |
+
"""argparse type for --temperature-by-type: 'choice=1.6,noul=1.2,score=1.8'."""
|
| 24 |
+
result = {}
|
| 25 |
+
for part in filter(None, (item.strip() for item in text.split(","))):
|
| 26 |
+
key, sep, value = part.partition("=")
|
| 27 |
+
key = key.strip()
|
| 28 |
+
if not sep or key not in ("choice", "noul", "score") or key in result:
|
| 29 |
+
raise argparse.ArgumentTypeError(
|
| 30 |
+
f"expected type=T pairs for choice, noul, score; got {part!r}"
|
| 31 |
+
)
|
| 32 |
+
result[key] = positive_temperature(value.strip())
|
| 33 |
+
return result
|
| 34 |
+
|
| 35 |
+
|
| 36 |
def main():
|
| 37 |
parser = argparse.ArgumentParser(description=__doc__)
|
| 38 |
parser.add_argument("--engine-url", default="http://127.0.0.1:30000")
|
|
|
|
| 54 |
"(label logits are divided by T before softmax; an explicit request "
|
| 55 |
"value wins). Env: JEV_DEFAULT_TEMPERATURE. Default 1.0.",
|
| 56 |
)
|
| 57 |
+
parser.add_argument(
|
| 58 |
+
"--temperature-by-type",
|
| 59 |
+
type=temperature_map,
|
| 60 |
+
default=os.environ.get("JEV_TEMPERATURE_BY_TYPE", ""),
|
| 61 |
+
help="Per-answer-type defaults used instead of --default-temperature when "
|
| 62 |
+
"a request omits options.temperature, e.g. 'choice=1.6,noul=1.2,score=1.8'. "
|
| 63 |
+
"Types not listed use --default-temperature. Env: JEV_TEMPERATURE_BY_TYPE.",
|
| 64 |
+
)
|
| 65 |
parser.add_argument(
|
| 66 |
"--prompt-wording",
|
| 67 |
choices=("served", "native"),
|
|
|
|
| 158 |
max_concurrency=args.max_concurrency,
|
| 159 |
alias=args.alias,
|
| 160 |
default_temperature=args.default_temperature,
|
| 161 |
+
temperature_by_type=args.temperature_by_type,
|
| 162 |
prompt_wording=args.prompt_wording,
|
| 163 |
)
|
| 164 |
uvicorn.run(app, host=args.host, port=args.port)
|
server/jev_adapter/server.py
CHANGED
|
@@ -23,6 +23,7 @@ def create_app(
|
|
| 23 |
max_concurrency: int = 32,
|
| 24 |
alias: str = "jev-latest",
|
| 25 |
default_temperature: float = 1.0,
|
|
|
|
| 26 |
prompt_wording: str = "served",
|
| 27 |
) -> FastAPI:
|
| 28 |
service = SystemOneService(
|
|
@@ -30,6 +31,7 @@ def create_app(
|
|
| 30 |
max_concurrency=max_concurrency,
|
| 31 |
alias=alias,
|
| 32 |
default_temperature=default_temperature,
|
|
|
|
| 33 |
prompt_wording=prompt_wording,
|
| 34 |
)
|
| 35 |
|
|
|
|
| 23 |
max_concurrency: int = 32,
|
| 24 |
alias: str = "jev-latest",
|
| 25 |
default_temperature: float = 1.0,
|
| 26 |
+
temperature_by_type: dict[str, float] | None = None,
|
| 27 |
prompt_wording: str = "served",
|
| 28 |
) -> FastAPI:
|
| 29 |
service = SystemOneService(
|
|
|
|
| 31 |
max_concurrency=max_concurrency,
|
| 32 |
alias=alias,
|
| 33 |
default_temperature=default_temperature,
|
| 34 |
+
temperature_by_type=temperature_by_type,
|
| 35 |
prompt_wording=prompt_wording,
|
| 36 |
)
|
| 37 |
|
server/jev_adapter/service.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
| 3 |
import asyncio
|
| 4 |
import math
|
| 5 |
import time
|
|
|
|
| 6 |
from numbers import Real
|
| 7 |
|
| 8 |
from .backend import AdapterError, ScoringBackend
|
|
@@ -15,6 +16,28 @@ from .protocol import (
|
|
| 15 |
)
|
| 16 |
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
class SystemOneService:
|
| 19 |
def __init__(
|
| 20 |
self,
|
|
@@ -23,6 +46,7 @@ class SystemOneService:
|
|
| 23 |
max_concurrency: int = 32,
|
| 24 |
alias: str = "jev-latest",
|
| 25 |
default_temperature: float = 1.0,
|
|
|
|
| 26 |
prompt_wording: str = "served",
|
| 27 |
):
|
| 28 |
if max_concurrency < 1:
|
|
@@ -41,6 +65,10 @@ class SystemOneService:
|
|
| 41 |
# request value always wins, and temperature_scaling=false still
|
| 42 |
# disables scaling. Without this setting the request default (1.0) holds.
|
| 43 |
self.default_temperature = float(default_temperature)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
# Server-wide, not per-request: which text build_prompt() renders.
|
| 45 |
# "native" additionally requires the backend's labels to be canonical
|
| 46 |
# A-Z (build_prompt raises otherwise) and relies on the backend to
|
|
@@ -55,6 +83,18 @@ class SystemOneService:
|
|
| 55 |
return options
|
| 56 |
return options.model_copy(update={"temperature": self.default_temperature})
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
async def score(self, request: SystemOneRequest) -> dict:
|
| 59 |
if request.model not in {self.backend.model, self.alias}:
|
| 60 |
raise AdapterError(
|
|
@@ -107,7 +147,9 @@ class SystemOneService:
|
|
| 107 |
return {
|
| 108 |
"model": self.backend.model,
|
| 109 |
"answers": {
|
| 110 |
-
key: reduce_probabilities(
|
|
|
|
|
|
|
| 111 |
for key, plan in plans.items()
|
| 112 |
},
|
| 113 |
"usage": {"input_tokens": input_tokens, "output_tokens": 0},
|
|
@@ -116,6 +158,13 @@ class SystemOneService:
|
|
| 116 |
"temperature": (
|
| 117 |
options.temperature if options.temperature_scaling else 1.0
|
| 118 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
"evaluations": len(jobs),
|
| 120 |
"adapter_elapsed_ms": (time.perf_counter() - started) * 1000,
|
| 121 |
},
|
|
|
|
| 3 |
import asyncio
|
| 4 |
import math
|
| 5 |
import time
|
| 6 |
+
from collections.abc import Mapping
|
| 7 |
from numbers import Real
|
| 8 |
|
| 9 |
from .backend import AdapterError, ScoringBackend
|
|
|
|
| 16 |
)
|
| 17 |
|
| 18 |
|
| 19 |
+
QUESTION_TYPES = ("choice", "noul", "score")
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def validate_temperature_by_type(values: Mapping[str, float] | None) -> dict[str, float]:
|
| 23 |
+
"""Check a {answer type: temperature} map; returns a plain dict (empty if None)."""
|
| 24 |
+
if values is None:
|
| 25 |
+
return {}
|
| 26 |
+
result = {}
|
| 27 |
+
for key, value in values.items():
|
| 28 |
+
if key not in QUESTION_TYPES:
|
| 29 |
+
raise ValueError(f"unknown answer type for temperature_by_type: {key!r}")
|
| 30 |
+
if (
|
| 31 |
+
isinstance(value, bool)
|
| 32 |
+
or not isinstance(value, Real)
|
| 33 |
+
or not math.isfinite(value)
|
| 34 |
+
or value <= 0
|
| 35 |
+
):
|
| 36 |
+
raise ValueError("temperature_by_type values must be finite and greater than zero")
|
| 37 |
+
result[key] = float(value)
|
| 38 |
+
return result
|
| 39 |
+
|
| 40 |
+
|
| 41 |
class SystemOneService:
|
| 42 |
def __init__(
|
| 43 |
self,
|
|
|
|
| 46 |
max_concurrency: int = 32,
|
| 47 |
alias: str = "jev-latest",
|
| 48 |
default_temperature: float = 1.0,
|
| 49 |
+
temperature_by_type: Mapping[str, float] | None = None,
|
| 50 |
prompt_wording: str = "served",
|
| 51 |
):
|
| 52 |
if max_concurrency < 1:
|
|
|
|
| 65 |
# request value always wins, and temperature_scaling=false still
|
| 66 |
# disables scaling. Without this setting the request default (1.0) holds.
|
| 67 |
self.default_temperature = float(default_temperature)
|
| 68 |
+
# Optional per-answer-type defaults (choice / noul / score); a type not
|
| 69 |
+
# listed falls back to default_temperature. Also applied only when the
|
| 70 |
+
# request omits options.temperature.
|
| 71 |
+
self.temperature_by_type = validate_temperature_by_type(temperature_by_type)
|
| 72 |
# Server-wide, not per-request: which text build_prompt() renders.
|
| 73 |
# "native" additionally requires the backend's labels to be canonical
|
| 74 |
# A-Z (build_prompt raises otherwise) and relies on the backend to
|
|
|
|
| 83 |
return options
|
| 84 |
return options.model_copy(update={"temperature": self.default_temperature})
|
| 85 |
|
| 86 |
+
def question_options(self, request: SystemOneRequest, options, question_type: str):
|
| 87 |
+
"""Options for one question: the per-type default when the request left
|
| 88 |
+
temperature unset and a default exists for this answer type."""
|
| 89 |
+
if (
|
| 90 |
+
"temperature" in request.options.model_fields_set
|
| 91 |
+
or question_type not in self.temperature_by_type
|
| 92 |
+
):
|
| 93 |
+
return options
|
| 94 |
+
return options.model_copy(
|
| 95 |
+
update={"temperature": self.temperature_by_type[question_type]}
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
async def score(self, request: SystemOneRequest) -> dict:
|
| 99 |
if request.model not in {self.backend.model, self.alias}:
|
| 100 |
raise AdapterError(
|
|
|
|
| 147 |
return {
|
| 148 |
"model": self.backend.model,
|
| 149 |
"answers": {
|
| 150 |
+
key: reduce_probabilities(
|
| 151 |
+
plan, *grouped[key], self.question_options(request, options, plan.type)
|
| 152 |
+
)
|
| 153 |
for key, plan in plans.items()
|
| 154 |
},
|
| 155 |
"usage": {"input_tokens": input_tokens, "output_tokens": 0},
|
|
|
|
| 158 |
"temperature": (
|
| 159 |
options.temperature if options.temperature_scaling else 1.0
|
| 160 |
),
|
| 161 |
+
**(
|
| 162 |
+
{"temperature_by_type": dict(self.temperature_by_type)}
|
| 163 |
+
if self.temperature_by_type
|
| 164 |
+
and "temperature" not in request.options.model_fields_set
|
| 165 |
+
and options.temperature_scaling
|
| 166 |
+
else {}
|
| 167 |
+
),
|
| 168 |
"evaluations": len(jobs),
|
| 169 |
"adapter_elapsed_ms": (time.perf_counter() - started) * 1000,
|
| 170 |
},
|
server/jev_adapter/sglang.py
CHANGED
|
@@ -31,6 +31,7 @@ def _label_candidates() -> list[str]:
|
|
| 31 |
|
| 32 |
|
| 33 |
_IMAGE_MODEL_TYPES = {
|
|
|
|
| 34 |
"qwen2_vl",
|
| 35 |
"qwen2_5_vl",
|
| 36 |
"qwen3_vl",
|
|
@@ -41,6 +42,7 @@ _IMAGE_MODEL_TYPES = {
|
|
| 41 |
"pixtral",
|
| 42 |
}
|
| 43 |
_IMAGE_ARCHITECTURES = {
|
|
|
|
| 44 |
"Qwen2VLForConditionalGeneration",
|
| 45 |
"Qwen2_5_VLForConditionalGeneration",
|
| 46 |
"Qwen3VLForConditionalGeneration",
|
|
@@ -335,7 +337,7 @@ class SGLangBackend:
|
|
| 335 |
if images and not self._image_format_supported:
|
| 336 |
raise AdapterError(
|
| 337 |
"image_model_not_supported",
|
| 338 |
-
"Image scoring currently supports Qwen VL/Qwen3.5 and Mistral3/Pixtral chat formats.",
|
| 339 |
"images",
|
| 340 |
)
|
| 341 |
if self._native_tokenizer is not None:
|
|
|
|
| 31 |
|
| 32 |
|
| 33 |
_IMAGE_MODEL_TYPES = {
|
| 34 |
+
"mage_vl",
|
| 35 |
"qwen2_vl",
|
| 36 |
"qwen2_5_vl",
|
| 37 |
"qwen3_vl",
|
|
|
|
| 42 |
"pixtral",
|
| 43 |
}
|
| 44 |
_IMAGE_ARCHITECTURES = {
|
| 45 |
+
"MageVLForConditionalGeneration",
|
| 46 |
"Qwen2VLForConditionalGeneration",
|
| 47 |
"Qwen2_5_VLForConditionalGeneration",
|
| 48 |
"Qwen3VLForConditionalGeneration",
|
|
|
|
| 337 |
if images and not self._image_format_supported:
|
| 338 |
raise AdapterError(
|
| 339 |
"image_model_not_supported",
|
| 340 |
+
"Image scoring currently supports Mage-VL, Qwen VL/Qwen3.5, and Mistral3/Pixtral chat formats.",
|
| 341 |
"images",
|
| 342 |
)
|
| 343 |
if self._native_tokenizer is not None:
|
server/tests/test_service.py
CHANGED
|
@@ -4,7 +4,7 @@ import unittest
|
|
| 4 |
|
| 5 |
from fastapi.testclient import TestClient
|
| 6 |
|
| 7 |
-
from jev_adapter.__main__ import positive_temperature
|
| 8 |
from jev_adapter.backend import AdapterError, ScoringResult
|
| 9 |
from jev_adapter.protocol import SystemOneRequest, probabilities_from_logprobs
|
| 10 |
from jev_adapter.server import create_app
|
|
@@ -203,6 +203,50 @@ class TestDefaultTemperature(unittest.IsolatedAsyncioTestCase):
|
|
| 203 |
2.0,
|
| 204 |
)
|
| 205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
def test_cli_default_temperature_type(self):
|
| 207 |
self.assertEqual(positive_temperature("2.4"), 2.4)
|
| 208 |
self.assertEqual(positive_temperature("1"), 1.0)
|
|
|
|
| 4 |
|
| 5 |
from fastapi.testclient import TestClient
|
| 6 |
|
| 7 |
+
from jev_adapter.__main__ import positive_temperature, temperature_map
|
| 8 |
from jev_adapter.backend import AdapterError, ScoringResult
|
| 9 |
from jev_adapter.protocol import SystemOneRequest, probabilities_from_logprobs
|
| 10 |
from jev_adapter.server import create_app
|
|
|
|
| 203 |
2.0,
|
| 204 |
)
|
| 205 |
|
| 206 |
+
async def test_temperature_by_type_applies_per_answer_type(self):
|
| 207 |
+
result = await self.score(
|
| 208 |
+
payload(), default_temperature=2.4, temperature_by_type={"score": 3.0, "noul": 1.5}
|
| 209 |
+
)
|
| 210 |
+
urgency = list(result["answers"]["urgency"]["probabilities"].values())
|
| 211 |
+
for actual, wanted in zip(urgency, probabilities_from_logprobs(self.vector, 3.0)):
|
| 212 |
+
self.assertAlmostEqual(actual, wanted)
|
| 213 |
+
route = list(result["answers"]["route"]["probabilities"].values())
|
| 214 |
+
for actual, wanted in zip(route, probabilities_from_logprobs(self.vector[:2], 2.4)):
|
| 215 |
+
self.assertAlmostEqual(actual, wanted)
|
| 216 |
+
self.assertEqual(result["metadata"]["temperature"], 2.4)
|
| 217 |
+
self.assertEqual(result["metadata"]["temperature_by_type"], {"score": 3.0, "noul": 1.5})
|
| 218 |
+
|
| 219 |
+
async def test_explicit_request_temperature_wins_over_temperature_by_type(self):
|
| 220 |
+
body = {**payload(), "options": {"temperature": 1.0}}
|
| 221 |
+
result = await self.score(body, temperature_by_type={"score": 3.0})
|
| 222 |
+
urgency = list(result["answers"]["urgency"]["probabilities"].values())
|
| 223 |
+
for actual, wanted in zip(urgency, probabilities_from_logprobs(self.vector, 1.0)):
|
| 224 |
+
self.assertAlmostEqual(actual, wanted)
|
| 225 |
+
self.assertNotIn("temperature_by_type", result["metadata"])
|
| 226 |
+
|
| 227 |
+
async def test_disabled_scaling_ignores_temperature_by_type(self):
|
| 228 |
+
body = {**payload(), "options": {"temperature_scaling": False}}
|
| 229 |
+
result = await self.score(body, temperature_by_type={"score": 3.0})
|
| 230 |
+
urgency = list(result["answers"]["urgency"]["probabilities"].values())
|
| 231 |
+
for actual, wanted in zip(urgency, probabilities_from_logprobs(self.vector, 1.0)):
|
| 232 |
+
self.assertAlmostEqual(actual, wanted)
|
| 233 |
+
self.assertNotIn("temperature_by_type", result["metadata"])
|
| 234 |
+
|
| 235 |
+
def test_invalid_temperature_by_type_rejected_at_construction(self):
|
| 236 |
+
for value in ({"judge": 1.2}, {"score": 0}, {"score": float("nan")}, {"noul": True}):
|
| 237 |
+
with self.assertRaises(ValueError):
|
| 238 |
+
SystemOneService(FakeBackend(), temperature_by_type=value)
|
| 239 |
+
self.assertEqual(SystemOneService(FakeBackend()).temperature_by_type, {})
|
| 240 |
+
|
| 241 |
+
def test_cli_temperature_map_type(self):
|
| 242 |
+
self.assertEqual(temperature_map("choice=1.6, noul=1.2,score=1.8"), {"choice": 1.6, "noul": 1.2, "score": 1.8})
|
| 243 |
+
self.assertEqual(temperature_map(""), {})
|
| 244 |
+
import argparse
|
| 245 |
+
|
| 246 |
+
for text in ("choice", "judge=1.2", "choice=0", "choice=1.2,choice=1.3", "noul=abc"):
|
| 247 |
+
with self.assertRaises(argparse.ArgumentTypeError):
|
| 248 |
+
temperature_map(text)
|
| 249 |
+
|
| 250 |
def test_cli_default_temperature_type(self):
|
| 251 |
self.assertEqual(positive_temperature("2.4"), 2.4)
|
| 252 |
self.assertEqual(positive_temperature("1"), 1.0)
|
server/tests/test_sglang.py
CHANGED
|
@@ -232,6 +232,36 @@ async def test_images_remain_in_native_request_with_template_and_prefix():
|
|
| 232 |
await client.aclose()
|
| 233 |
|
| 234 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
@pytest.mark.asyncio
|
| 236 |
@pytest.mark.parametrize(
|
| 237 |
"field,value,code",
|
|
|
|
| 232 |
await client.aclose()
|
| 233 |
|
| 234 |
|
| 235 |
+
@pytest.mark.asyncio
|
| 236 |
+
@pytest.mark.parametrize(
|
| 237 |
+
"model_type,architectures",
|
| 238 |
+
[
|
| 239 |
+
("mage_vl", []),
|
| 240 |
+
("unknown", ["MageVLForConditionalGeneration"]),
|
| 241 |
+
],
|
| 242 |
+
)
|
| 243 |
+
async def test_mage_vl_image_capability_and_message_format(model_type, architectures):
|
| 244 |
+
engine = Engine()
|
| 245 |
+
engine.info.update(model_type=model_type, architectures=architectures)
|
| 246 |
+
backend, engine, client = await make_backend(engine)
|
| 247 |
+
try:
|
| 248 |
+
image = "data:image/png;base64,YQ=="
|
| 249 |
+
labels, tokens = backend.labels(2)
|
| 250 |
+
await backend.evaluate("describe", [image], labels, tokens, None)
|
| 251 |
+
assert engine.calls[-4][1]["messages"] == [
|
| 252 |
+
{
|
| 253 |
+
"role": "user",
|
| 254 |
+
"content": [
|
| 255 |
+
{"type": "image_url", "image_url": {"url": image}},
|
| 256 |
+
{"type": "text", "text": "describe"},
|
| 257 |
+
],
|
| 258 |
+
}
|
| 259 |
+
]
|
| 260 |
+
assert engine.calls[-1][1]["image_data"] == [image]
|
| 261 |
+
finally:
|
| 262 |
+
await client.aclose()
|
| 263 |
+
|
| 264 |
+
|
| 265 |
@pytest.mark.asyncio
|
| 266 |
@pytest.mark.parametrize(
|
| 267 |
"field,value,code",
|