MyeongHoJeong commited on
Commit
3c08bbf
·
verified ·
1 Parent(s): 16767f1

Add serving code

Browse files
SHA256SUMS CHANGED
@@ -34,7 +34,7 @@ f9913e0d49fc053df656058d13af2523b6263f9396efad7a6fa06e7a121e1acf release-manife
34
  eef2a88a000d2ec294379755770c147327cb79d3d7278c711899bd36797d7a02 server/.gitignore
35
  1495e1e757ef4d0925a2350563cf5754bb23c51701a8ec4fb3c5cdcbedae6747 server/LICENSE
36
  0f1f023b916d38d7c12af3a71e09f5e82b819a7e156e4992118bb160ee63ede1 server/NOTICE
37
- 56c89049984cec6fa66a5bea6b905c63aa46fbd627ba12bbfabac1a6b466d321 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,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
- c79719983fcf12ad2baa020acb1be00e3b1e4d4b66e7dcaa0a8022f140975f6a 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,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
- 3eb21e967cbc8dd7dec2481553fce6974be66276d196eb791f0292bc95c2c8f0 server/jev_adapter/server.py
71
- bd5e5a13fb264c1e528132c3d33bb2b85c31bc45bb0d025d8151ea47216dbff4 server/jev_adapter/service.py
72
- 8b025570f9678aae6717c817a94e807c21a2c0fe1eb82708a611d3f67be8c3e9 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,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
- 200c35bf53fd869132a69cb8adfd32a1f8e7235d0a7194b19675e46ac39c936b server/tests/test_service.py
84
- 50af7340ac40f3179403b3c53779f673c4ff1be888a015c2f8852731b5f69f0b server/tests/test_sglang.py
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`로 서빙합니다(시작 시 경고 로그). `/opt/.../SYSTEM_PROMPT.txt` 같은 정적
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(plan, *grouped[key], options)
 
 
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",