intel-npu-server 0.3.0__tar.gz

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.
@@ -0,0 +1,186 @@
1
+ Metadata-Version: 2.4
2
+ Name: intel-npu-server
3
+ Version: 0.3.0
4
+ Summary: Local OpenAI-compatible server for OpenVINO GenAI models (primarily on Intel NPU)
5
+ Author: Alex Spirin
6
+ License-Expression: MIT
7
+ Requires-Dist: fastapi>=0.115,<1
8
+ Requires-Dist: huggingface-hub>=0.34,<2
9
+ Requires-Dist: openvino-genai==2026.4.1.0
10
+ Requires-Dist: pydantic>=2.10,<3
11
+ Requires-Dist: transformers==5.20.0
12
+ Requires-Dist: uvicorn[standard]>=0.34,<1
13
+ Requires-Dist: optimum-intel>=2.0.0
14
+ Requires-Python: >=3.12, <3.13
15
+ Description-Content-Type: text/markdown
16
+
17
+ # LLM on Intel NPU / OpenVINO GenAI / OpenAI-compatible API
18
+
19
+ Modular local inference server for OpenCode. The project uses Python 3.12 and `uv`; no PowerShell or batch scripts are required. LFM3 is the default checkpoint, but any exportable OpenVINO model works (see section 4.1).
20
+
21
+ ## Project layout
22
+
23
+ ```text
24
+ .
25
+ |-- pyproject.toml
26
+ |-- uv.lock # generated by uv lock / uv sync
27
+ |-- model_configuration.json # generated automatically
28
+ |-- opencode.jsonc
29
+ `-- npu_server/
30
+ |-- __main__.py # thin entry point
31
+ |-- tool_call_parsers.py
32
+ |-- cli/
33
+ | |-- arguments_cli.py # ModelArgumentsParser
34
+ | |-- model_config_cli.py # ModelConfig
35
+ | `-- server_cli.py # CommandLineInterface
36
+ |-- generation/
37
+ | |-- events.py # StreamEvent, GenerationRequest, GenerationOutcome
38
+ | |-- streaming.py # ToolSafeStreamBuffer, TokenStreamCallback
39
+ | `-- engine.py # GenerationEngine
40
+ |-- load_model/
41
+ | |-- runtime.py # GenerationError, ModelRuntime, RuntimeLengths
42
+ | |-- preparer.py # OpenvinoModelPreparer (download + IR export)
43
+ | `-- loader.py # OpenvinoModelLoader (pipeline compilation)
44
+ `-- server/
45
+ |-- openai_schemas.py # CompletionResult, ChatCompletionChunkBuilder
46
+ |-- message_normalizer.py# ChatMessageNormalizer
47
+ |-- http_app.py # NpuServer (routes + uvicorn)
48
+ ```
49
+
50
+ `model_dir` defaults to `Path.cwd().parent / "model"`. The resolved path is written into `model_configuration.json` even when `--model-dir` is omitted. This ensures that a later `serve` command opens the same exported model directory.
51
+
52
+ Each model lives in its own subfolder named after `model_name`, so several models can share one `model_dir`:
53
+
54
+ ```
55
+ model/
56
+ `-- lfm3-uncensored/
57
+ |-- checkpoint/ # source Hugging Face checkpoint (downloaded once)
58
+ `-- exported/ # OpenVINO IR, tokenizer files, model_manifest.json
59
+ ```
60
+
61
+ The global Hugging Face cache is never used. A legacy flat layout (`model_dir\<files>` + `model_dir\checkpoint`) is migrated automatically on the first start of the new version.
62
+
63
+ ## 1. Install and synchronize
64
+
65
+ Install `uv` once using the official installer for your operating system. Then, from the project root:
66
+
67
+ ```powershell
68
+ uv python install 3.12
69
+ uv sync
70
+ ```
71
+
72
+ The `optimum-intel` dependency is installed from the author's `lfm3` development branch, and `transformers==5.20.0` is used as required by the LFM3 support PR. The package metadata on that branch may still declare an older upper bound, so `tool.uv.override-dependencies` explicitly overrides it. This resolves the dependency solver conflict; it does not itself guarantee runtime compatibility.
73
+
74
+ ## 2. Run the server
75
+
76
+ First launch:
77
+
78
+ ```powershell
79
+ uv run python -m npu_server run
80
+ ```
81
+
82
+ The `run` command:
83
+
84
+ 1. Reads the configuration file and applies command-line overrides.
85
+ 2. Migrates a legacy flat model layout if one is found.
86
+ 3. Downloads the Hugging Face checkpoint into `model_dir\<model_name>\checkpoint` (unless `--model-id` points to a local directory).
87
+ 4. Loads the tokenizer from that checkpoint.
88
+ 5. Invokes the Optimum Intel Python CLI entry point in-process to export OpenVINO IR using INT4 symmetric weight compression and group size 128.
89
+ 6. Saves the tokenizer into `model_dir\<model_name>\exported` next to the IR files.
90
+ 7. Compiles/loads `openvino_genai.LLMPipeline` on the selected device. With the default `auto` lengths on NPU it tries `MAX_PROMPT_LEN=8192, MIN_RESPONSE_LEN=1024` first and falls back down the ladder `4096/1024 -> 2048/512 -> 1024/256` when the device refuses to compile; explicit `--max-prompt-len`/`--min-response-len` values disable the ladder.
91
+ 8. Starts the HTTP server and keeps the model in memory until the process is stopped with Ctrl+C. On shutdown (Ctrl+C or uvicorn graceful exit) the pipeline reference is dropped and garbage-collected, releasing the model and its memory from the device before the process exits.
92
+
93
+ Subsequent `run` calls reuse an existing `.xml` + `.bin` IR export and the saved tokenizer.
94
+
95
+ ## 3. Individual commands
96
+
97
+ Export only:
98
+
99
+ ```powershell
100
+ uv run python -m npu_server prepare
101
+ ```
102
+
103
+ Run the HTTP server using an existing IR export:
104
+
105
+ ```powershell
106
+ uv run python -m npu_server serve
107
+ ```
108
+
109
+ Check the local OpenVINO installation and device visibility:
110
+
111
+ ```powershell
112
+ uv run python -m npu_server doctor
113
+ ```
114
+
115
+ Force a fresh export:
116
+
117
+ ```powershell
118
+ uv run python -m npu_server run --force-export
119
+ ```
120
+
121
+ ## 4. Configuration overrides
122
+
123
+ All values can be stored in `model_configuration.json` or passed as command-line arguments. Command-line arguments take precedence over the JSON file. If a value is omitted, the value from JSON is used; otherwise the `ModelConfig` default is used.
124
+
125
+ `model_id` and `device` are required: they must be present in `model_configuration.json` or passed as command-line arguments, otherwise startup fails with an error. `model_name` defaults to `model_id` when omitted.
126
+
127
+ ```powershell
128
+ uv run python -m npu_server run `
129
+ --model-id OpenIntelligenceNet/LFM3-1.7B-Uncensored `
130
+ --model-dir ..\model `
131
+ --device NPU `
132
+ --host 127.0.0.1 `
133
+ --port 8000 `
134
+ --max-prompt-len 1024 `
135
+ --min-response-len 128 `
136
+ --max-output-tokens 128
137
+ ```
138
+
139
+ `--model-dir` is optional. If omitted, it defaults to the resolved path `Path.cwd().parent / "model"`, and that value is persisted in `model_configuration.json`.
140
+
141
+ Length parameters accept either a positive integer or `auto`. On NPU, `auto` resolves to safe defaults (`MAX_PROMPT_LEN=8192`, `MIN_RESPONSE_LEN=1024`) with an automatic fallback ladder when the device refuses to compile. On CPU/GPU, `auto` omits the corresponding OpenVINO property so shapes stay dynamic; `max_output_tokens=auto` leaves the generation-length limit to the library default. For tight NPU memory budgets set the lengths explicitly.
142
+
143
+ ## 4.1. Other models
144
+
145
+ The server works with any Hugging Face checkpoint that Optimum Intel can export to OpenVINO IR; LFM3 is only the default. Pass `--model-id <repo>`, set `--model-name`, and match the tool-call format if the model supports tools:
146
+
147
+ `--tool-parser` selects the converter for the model's tool-call markup:
148
+
149
+ | value | markup |
150
+ | --- | --- |
151
+ | `auto` | guess from the model id/name: `lfm*` -> lfm3, `qwen`/`hermes`/`glm` -> hermes, otherwise none |
152
+ | `lfm3` | LFM2/LFM3 XML-ish `<function=...><parameter=...>` blocks |
153
+ | `hermes` | JSON payloads `{"name": ..., "arguments": ...}` (Qwen/Hermes style) |
154
+ | `none` | no tool-call parsing (text-only) |
155
+
156
+ `--enable-thinking` / `--no-enable-thinking` controls the `enable_thinking` chat-template flag for thinking-capable models.
157
+
158
+ `--full-attention-only` converts `sliding_attention` layers to `full_attention` in the downloaded checkpoint before export (removes `sliding_window`, keeps the `full_attention` RoPE group). Intel NPU (NPUW) fails to compile hybrid sliding/full attention models with `NPUW: conflicting attention mask detection for the same SDPA node`; this flag unifies the attention pattern. It changes the attention semantics of the sliding layers (RoPE theta reverts to the `full_attention` group), so use it only when the NPU compile fails without it. The flag is persisted in `model_configuration.json`, recorded in `model_manifest.json`, and requires `--force-export` to re-export an existing IR.
159
+
160
+ ## 5. HTTP API
161
+
162
+ The server binds to `127.0.0.1:8000` by default and exposes:
163
+
164
+ - `GET /health`
165
+ - `GET /v1/models`
166
+ - `POST /v1/chat/completions`
167
+
168
+ Example health request:
169
+
170
+ ```powershell
171
+ curl.exe http://127.0.0.1:8000/health
172
+ ```
173
+
174
+ The API applies the Hugging Face tokenizer's chat template and converts the model's tool-call markup into OpenAI-compatible `tool_calls`. Streaming responses are emitted token-by-token through an OpenVINO GenAI streamer; fragments that might belong to a tool-call block are withheld by a holdback buffer and flushed after generation, so tool calls parse reliably.
175
+
176
+ ## 6. OpenCode
177
+
178
+ Merge the contents of `opencode.jsonc` into your OpenCode configuration. The configured model is `local/lfm3-npu` and the base URL is `http://127.0.0.1:8000/v1`.
179
+
180
+ ## Technical notes
181
+
182
+ - OpenVINO's current NPU guidance recommends 4-bit symmetric compression and group-wise quantization (group size 128 for smaller models). The exporter uses these settings.
183
+ - NPU pipeline configuration uses `CACHE_DIR` always; `MAX_PROMPT_LEN`/`MIN_RESPONSE_LEN` default to 8192/1024 on NPU (`auto`) and are compiled with a fallback ladder; explicit values disable the ladder.
184
+ - Works with any checkpoint Optimum Intel can export; match `--tool-parser` to the model's tool-call markup. Validate export, tokenizer behavior, tool calls, and long-context behavior on the target device.
185
+ - `trust_remote_code=True` permits repository-provided Python code to run locally. Use it only with a repository whose code you trust.
186
+ - `uv.lock` is machine-generated; create it after reviewing the project dependencies with `uv lock`.
@@ -0,0 +1,170 @@
1
+ # LLM on Intel NPU / OpenVINO GenAI / OpenAI-compatible API
2
+
3
+ Modular local inference server for OpenCode. The project uses Python 3.12 and `uv`; no PowerShell or batch scripts are required. LFM3 is the default checkpoint, but any exportable OpenVINO model works (see section 4.1).
4
+
5
+ ## Project layout
6
+
7
+ ```text
8
+ .
9
+ |-- pyproject.toml
10
+ |-- uv.lock # generated by uv lock / uv sync
11
+ |-- model_configuration.json # generated automatically
12
+ |-- opencode.jsonc
13
+ `-- npu_server/
14
+ |-- __main__.py # thin entry point
15
+ |-- tool_call_parsers.py
16
+ |-- cli/
17
+ | |-- arguments_cli.py # ModelArgumentsParser
18
+ | |-- model_config_cli.py # ModelConfig
19
+ | `-- server_cli.py # CommandLineInterface
20
+ |-- generation/
21
+ | |-- events.py # StreamEvent, GenerationRequest, GenerationOutcome
22
+ | |-- streaming.py # ToolSafeStreamBuffer, TokenStreamCallback
23
+ | `-- engine.py # GenerationEngine
24
+ |-- load_model/
25
+ | |-- runtime.py # GenerationError, ModelRuntime, RuntimeLengths
26
+ | |-- preparer.py # OpenvinoModelPreparer (download + IR export)
27
+ | `-- loader.py # OpenvinoModelLoader (pipeline compilation)
28
+ `-- server/
29
+ |-- openai_schemas.py # CompletionResult, ChatCompletionChunkBuilder
30
+ |-- message_normalizer.py# ChatMessageNormalizer
31
+ |-- http_app.py # NpuServer (routes + uvicorn)
32
+ ```
33
+
34
+ `model_dir` defaults to `Path.cwd().parent / "model"`. The resolved path is written into `model_configuration.json` even when `--model-dir` is omitted. This ensures that a later `serve` command opens the same exported model directory.
35
+
36
+ Each model lives in its own subfolder named after `model_name`, so several models can share one `model_dir`:
37
+
38
+ ```
39
+ model/
40
+ `-- lfm3-uncensored/
41
+ |-- checkpoint/ # source Hugging Face checkpoint (downloaded once)
42
+ `-- exported/ # OpenVINO IR, tokenizer files, model_manifest.json
43
+ ```
44
+
45
+ The global Hugging Face cache is never used. A legacy flat layout (`model_dir\<files>` + `model_dir\checkpoint`) is migrated automatically on the first start of the new version.
46
+
47
+ ## 1. Install and synchronize
48
+
49
+ Install `uv` once using the official installer for your operating system. Then, from the project root:
50
+
51
+ ```powershell
52
+ uv python install 3.12
53
+ uv sync
54
+ ```
55
+
56
+ The `optimum-intel` dependency is installed from the author's `lfm3` development branch, and `transformers==5.20.0` is used as required by the LFM3 support PR. The package metadata on that branch may still declare an older upper bound, so `tool.uv.override-dependencies` explicitly overrides it. This resolves the dependency solver conflict; it does not itself guarantee runtime compatibility.
57
+
58
+ ## 2. Run the server
59
+
60
+ First launch:
61
+
62
+ ```powershell
63
+ uv run python -m npu_server run
64
+ ```
65
+
66
+ The `run` command:
67
+
68
+ 1. Reads the configuration file and applies command-line overrides.
69
+ 2. Migrates a legacy flat model layout if one is found.
70
+ 3. Downloads the Hugging Face checkpoint into `model_dir\<model_name>\checkpoint` (unless `--model-id` points to a local directory).
71
+ 4. Loads the tokenizer from that checkpoint.
72
+ 5. Invokes the Optimum Intel Python CLI entry point in-process to export OpenVINO IR using INT4 symmetric weight compression and group size 128.
73
+ 6. Saves the tokenizer into `model_dir\<model_name>\exported` next to the IR files.
74
+ 7. Compiles/loads `openvino_genai.LLMPipeline` on the selected device. With the default `auto` lengths on NPU it tries `MAX_PROMPT_LEN=8192, MIN_RESPONSE_LEN=1024` first and falls back down the ladder `4096/1024 -> 2048/512 -> 1024/256` when the device refuses to compile; explicit `--max-prompt-len`/`--min-response-len` values disable the ladder.
75
+ 8. Starts the HTTP server and keeps the model in memory until the process is stopped with Ctrl+C. On shutdown (Ctrl+C or uvicorn graceful exit) the pipeline reference is dropped and garbage-collected, releasing the model and its memory from the device before the process exits.
76
+
77
+ Subsequent `run` calls reuse an existing `.xml` + `.bin` IR export and the saved tokenizer.
78
+
79
+ ## 3. Individual commands
80
+
81
+ Export only:
82
+
83
+ ```powershell
84
+ uv run python -m npu_server prepare
85
+ ```
86
+
87
+ Run the HTTP server using an existing IR export:
88
+
89
+ ```powershell
90
+ uv run python -m npu_server serve
91
+ ```
92
+
93
+ Check the local OpenVINO installation and device visibility:
94
+
95
+ ```powershell
96
+ uv run python -m npu_server doctor
97
+ ```
98
+
99
+ Force a fresh export:
100
+
101
+ ```powershell
102
+ uv run python -m npu_server run --force-export
103
+ ```
104
+
105
+ ## 4. Configuration overrides
106
+
107
+ All values can be stored in `model_configuration.json` or passed as command-line arguments. Command-line arguments take precedence over the JSON file. If a value is omitted, the value from JSON is used; otherwise the `ModelConfig` default is used.
108
+
109
+ `model_id` and `device` are required: they must be present in `model_configuration.json` or passed as command-line arguments, otherwise startup fails with an error. `model_name` defaults to `model_id` when omitted.
110
+
111
+ ```powershell
112
+ uv run python -m npu_server run `
113
+ --model-id OpenIntelligenceNet/LFM3-1.7B-Uncensored `
114
+ --model-dir ..\model `
115
+ --device NPU `
116
+ --host 127.0.0.1 `
117
+ --port 8000 `
118
+ --max-prompt-len 1024 `
119
+ --min-response-len 128 `
120
+ --max-output-tokens 128
121
+ ```
122
+
123
+ `--model-dir` is optional. If omitted, it defaults to the resolved path `Path.cwd().parent / "model"`, and that value is persisted in `model_configuration.json`.
124
+
125
+ Length parameters accept either a positive integer or `auto`. On NPU, `auto` resolves to safe defaults (`MAX_PROMPT_LEN=8192`, `MIN_RESPONSE_LEN=1024`) with an automatic fallback ladder when the device refuses to compile. On CPU/GPU, `auto` omits the corresponding OpenVINO property so shapes stay dynamic; `max_output_tokens=auto` leaves the generation-length limit to the library default. For tight NPU memory budgets set the lengths explicitly.
126
+
127
+ ## 4.1. Other models
128
+
129
+ The server works with any Hugging Face checkpoint that Optimum Intel can export to OpenVINO IR; LFM3 is only the default. Pass `--model-id <repo>`, set `--model-name`, and match the tool-call format if the model supports tools:
130
+
131
+ `--tool-parser` selects the converter for the model's tool-call markup:
132
+
133
+ | value | markup |
134
+ | --- | --- |
135
+ | `auto` | guess from the model id/name: `lfm*` -> lfm3, `qwen`/`hermes`/`glm` -> hermes, otherwise none |
136
+ | `lfm3` | LFM2/LFM3 XML-ish `<function=...><parameter=...>` blocks |
137
+ | `hermes` | JSON payloads `{"name": ..., "arguments": ...}` (Qwen/Hermes style) |
138
+ | `none` | no tool-call parsing (text-only) |
139
+
140
+ `--enable-thinking` / `--no-enable-thinking` controls the `enable_thinking` chat-template flag for thinking-capable models.
141
+
142
+ `--full-attention-only` converts `sliding_attention` layers to `full_attention` in the downloaded checkpoint before export (removes `sliding_window`, keeps the `full_attention` RoPE group). Intel NPU (NPUW) fails to compile hybrid sliding/full attention models with `NPUW: conflicting attention mask detection for the same SDPA node`; this flag unifies the attention pattern. It changes the attention semantics of the sliding layers (RoPE theta reverts to the `full_attention` group), so use it only when the NPU compile fails without it. The flag is persisted in `model_configuration.json`, recorded in `model_manifest.json`, and requires `--force-export` to re-export an existing IR.
143
+
144
+ ## 5. HTTP API
145
+
146
+ The server binds to `127.0.0.1:8000` by default and exposes:
147
+
148
+ - `GET /health`
149
+ - `GET /v1/models`
150
+ - `POST /v1/chat/completions`
151
+
152
+ Example health request:
153
+
154
+ ```powershell
155
+ curl.exe http://127.0.0.1:8000/health
156
+ ```
157
+
158
+ The API applies the Hugging Face tokenizer's chat template and converts the model's tool-call markup into OpenAI-compatible `tool_calls`. Streaming responses are emitted token-by-token through an OpenVINO GenAI streamer; fragments that might belong to a tool-call block are withheld by a holdback buffer and flushed after generation, so tool calls parse reliably.
159
+
160
+ ## 6. OpenCode
161
+
162
+ Merge the contents of `opencode.jsonc` into your OpenCode configuration. The configured model is `local/lfm3-npu` and the base URL is `http://127.0.0.1:8000/v1`.
163
+
164
+ ## Technical notes
165
+
166
+ - OpenVINO's current NPU guidance recommends 4-bit symmetric compression and group-wise quantization (group size 128 for smaller models). The exporter uses these settings.
167
+ - NPU pipeline configuration uses `CACHE_DIR` always; `MAX_PROMPT_LEN`/`MIN_RESPONSE_LEN` default to 8192/1024 on NPU (`auto`) and are compiled with a fallback ladder; explicit values disable the ladder.
168
+ - Works with any checkpoint Optimum Intel can export; match `--tool-parser` to the model's tool-call markup. Validate export, tokenizer behavior, tool calls, and long-context behavior on the target device.
169
+ - `trust_remote_code=True` permits repository-provided Python code to run locally. Use it only with a repository whose code you trust.
170
+ - `uv.lock` is machine-generated; create it after reviewing the project dependencies with `uv lock`.
File without changes
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ from npu_server.cli.server_cli import CommandLineInterface
6
+
7
+
8
+ def main() -> int:
9
+ return CommandLineInterface().startup(sys.argv[1:])
10
+
11
+
12
+ if __name__ == "__main__":
13
+ raise SystemExit(main())
@@ -0,0 +1,2 @@
1
+ from npu_server.cli.arguments_cli import ModelArgumentsParser
2
+ from npu_server.cli.model_config_cli import CONFIG_FILENAME, MANIFEST_FILENAME, ModelConfig
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any, Literal, Sequence, cast
7
+
8
+ from npu_server.cli.model_config_cli import CONFIG_FILENAME, ModelConfig
9
+
10
+
11
+ class ModelArgumentsParser:
12
+ """Read model_configuration.json and apply command-line overrides onto ModelConfig."""
13
+
14
+ @staticmethod
15
+ def _load_config() -> tuple[dict[str, Any] | None, Path]:
16
+ """Load config from cwd, then its parent; otherwise return the cwd target path."""
17
+ cwd = Path.cwd().resolve()
18
+ candidates = (cwd / CONFIG_FILENAME, cwd.parent / CONFIG_FILENAME)
19
+
20
+ for config_path in candidates:
21
+ if config_path.is_file():
22
+ try:
23
+ raw_config: Any = json.loads(config_path.read_text(encoding="utf-8-sig"))
24
+ except json.JSONDecodeError as exc:
25
+ raise ValueError(f"Invalid JSON in configuration file {config_path}: {exc}") from exc
26
+
27
+ if not isinstance(raw_config, dict):
28
+ raise ValueError(f"Configuration root must be a JSON object: {config_path}")
29
+ return cast(dict[str, Any], raw_config), config_path
30
+
31
+ return None, candidates[0]
32
+
33
+ @staticmethod
34
+ def _parse_int_or_auto(value: str) -> int | Literal["auto"]:
35
+ if value.strip().lower() == "auto":
36
+ return "auto"
37
+ try:
38
+ return int(value)
39
+ except ValueError as exc:
40
+ raise argparse.ArgumentTypeError("Expected a positive integer or 'auto'") from exc
41
+
42
+ @staticmethod
43
+ def get_model_params(argv: Sequence[str] | None = None) -> ModelConfig:
44
+ config_data, config_path = ModelArgumentsParser._load_config()
45
+ usable_config = {key: value for key, value in (config_data or {}).items() if value is not None}
46
+ defaults = ModelConfig.model_validate(usable_config)
47
+
48
+ parser = argparse.ArgumentParser(description="Configuration options for the local OpenVINO model server")
49
+
50
+ parser.add_argument(
51
+ "--model-id",
52
+ "--model",
53
+ "-m",
54
+ dest="model_id",
55
+ type=str,
56
+ default=defaults.model_id,
57
+ help="Hugging Face repository ID or local model directory",
58
+ )
59
+ parser.add_argument("--model-dir", type=Path, default=defaults.model_dir, help="Path to save/load model")
60
+ parser.add_argument(
61
+ "--model-name",
62
+ dest="model_name",
63
+ type=str,
64
+ default=defaults.model_name,
65
+ help="Model name exposed by the /v1/models endpoint",
66
+ )
67
+ parser.add_argument("--device", default=defaults.device)
68
+ parser.add_argument(
69
+ "--tool-parser",
70
+ dest="tool_parser",
71
+ default=defaults.tool_parser,
72
+ help="Tool call markup converter: auto, lfm3, hermes, none",
73
+ )
74
+ parser.add_argument(
75
+ "--enable-thinking",
76
+ dest="enable_thinking",
77
+ action=argparse.BooleanOptionalAction,
78
+ default=defaults.enable_thinking,
79
+ help="Pass enable_thinking to the chat template",
80
+ )
81
+ parser.add_argument(
82
+ "--full-attention-only",
83
+ dest="full_attention_only",
84
+ action=argparse.BooleanOptionalAction,
85
+ default=defaults.full_attention_only,
86
+ help="Unify all attention layers to full_attention before export (NPU/NPUW fix)",
87
+ )
88
+ parser.add_argument("--host", default=defaults.host)
89
+ parser.add_argument("--port", type=int, default=defaults.port)
90
+ parser.add_argument(
91
+ "--max-prompt-len",
92
+ type=ModelArgumentsParser._parse_int_or_auto,
93
+ default=defaults.max_prompt_len,
94
+ )
95
+ parser.add_argument(
96
+ "--min-response-len",
97
+ type=ModelArgumentsParser._parse_int_or_auto,
98
+ default=defaults.min_response_len,
99
+ )
100
+ parser.add_argument(
101
+ "--max-output-tokens",
102
+ type=ModelArgumentsParser._parse_int_or_auto,
103
+ default=defaults.max_output_tokens,
104
+ )
105
+
106
+ args = parser.parse_args(argv)
107
+ validated_config = ModelConfig.model_validate(vars(args))
108
+ validated_config.save_config(config_path)
109
+
110
+ return validated_config
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Literal
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
8
+
9
+ from npu_server.tool_call_parsers import TOOL_PARSER_NAMES
10
+
11
+ CONFIG_FILENAME = "model_configuration.json"
12
+ MANIFEST_FILENAME = "model_manifest.json"
13
+
14
+
15
+ class ModelConfig(BaseModel):
16
+ """Runtime configuration persisted in model_configuration.json."""
17
+
18
+ model_config = ConfigDict(extra="ignore")
19
+
20
+ model_id: str = Field(description="Hugging Face repository ID or local model directory (required)")
21
+ model_dir: Path = Field(
22
+ default=(Path.cwd().parent / "model").resolve(),
23
+ description="Directory containing the exported OpenVINO IR and tokenizer",
24
+ )
25
+ model_name: str | None = Field(
26
+ default=None,
27
+ description="Model ID exposed by the /v1/models endpoint; defaults to model_id",
28
+ )
29
+ device: str = Field(
30
+ description="OpenVINO device (NPU, CPU, AUTO, ...) (required)",
31
+ )
32
+ host: str = Field(default="127.0.0.1", description="HTTP server bind address")
33
+ port: int = Field(default=8000, ge=1, le=65535)
34
+
35
+ tool_parser: str = Field(
36
+ default="auto",
37
+ description="Tool call markup format: auto, lfm3, hermes, none",
38
+ )
39
+ enable_thinking: bool = Field(
40
+ default=False,
41
+ description="Pass enable_thinking to the chat template (thinking-capable models)",
42
+ )
43
+ full_attention_only: bool = Field(
44
+ default=False,
45
+ description=(
46
+ "Convert sliding_attention layers to full_attention before export; "
47
+ "required for hybrid sliding/full LFM checkpoints on Intel NPU (NPUW "
48
+ "conflicting attention mask assertion)"
49
+ ),
50
+ )
51
+
52
+ max_prompt_len: int | Literal["auto"] = Field(
53
+ default="auto",
54
+ description="Maximum prompt length for the NPU pipeline, or 'auto'",
55
+ )
56
+ min_response_len: int | Literal["auto"] = Field(
57
+ default="auto",
58
+ description="Minimum response length reserved by the NPU pipeline, or 'auto'",
59
+ )
60
+ max_output_tokens: int | Literal["auto"] = Field(
61
+ default="auto",
62
+ description="Maximum generated tokens per request, or 'auto'",
63
+ )
64
+
65
+ @field_validator("model_id", "device", "host")
66
+ @classmethod
67
+ def validate_non_empty_strings(cls, value: str) -> str:
68
+ value = value.strip()
69
+ if not value:
70
+ raise ValueError("Value must not be empty")
71
+ return value
72
+
73
+ @field_validator("model_dir")
74
+ @classmethod
75
+ def normalize_model_dir(cls, value: Path) -> Path:
76
+ value = Path(value).expanduser()
77
+ value.mkdir(parents=True, exist_ok=True)
78
+ return value.resolve()
79
+
80
+ @field_validator("tool_parser")
81
+ @classmethod
82
+ def validate_tool_parser(cls, value: str) -> str:
83
+ value = value.strip().lower()
84
+ if value not in TOOL_PARSER_NAMES:
85
+ raise ValueError(f"tool_parser must be one of: {sorted(TOOL_PARSER_NAMES)}")
86
+ return value
87
+
88
+ @field_validator("max_prompt_len", "min_response_len", "max_output_tokens")
89
+ @classmethod
90
+ def validate_positive_lengths(cls, value: int | Literal["auto"]) -> int | Literal["auto"]:
91
+ if (isinstance(value, int) and value <= 0) or (isinstance(value, str) and value != "auto"):
92
+ raise ValueError("Length values must be positive integers or 'auto'")
93
+ return value
94
+
95
+ def save_config(self, path: str | Path) -> None:
96
+ config_path = Path(path).expanduser().resolve()
97
+ if config_path.suffix == ".json" and config_path.name == CONFIG_FILENAME:
98
+ config_file = config_path
99
+ else:
100
+ config_file = config_path / CONFIG_FILENAME
101
+ config_file.parent.mkdir(parents=True, exist_ok=True)
102
+ config_file.write_text(
103
+ json.dumps(self.model_dump(mode="json"), ensure_ascii=False, indent=1) + "\n",
104
+ encoding="utf-8",
105
+ )
106
+
107
+ @model_validator(mode="after")
108
+ def derive_model_name(self) -> ModelConfig:
109
+ if self.model_name is None:
110
+ self.model_name = self.model_id
111
+ return self