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.
- intel_npu_server-0.3.0/PKG-INFO +186 -0
- intel_npu_server-0.3.0/README.md +170 -0
- intel_npu_server-0.3.0/npu_server/__init__.py +0 -0
- intel_npu_server-0.3.0/npu_server/__main__.py +13 -0
- intel_npu_server-0.3.0/npu_server/cli/__init__.py +2 -0
- intel_npu_server-0.3.0/npu_server/cli/arguments_cli.py +110 -0
- intel_npu_server-0.3.0/npu_server/cli/model_config_cli.py +111 -0
- intel_npu_server-0.3.0/npu_server/cli/server_cli.py +84 -0
- intel_npu_server-0.3.0/npu_server/generation/__init__.py +3 -0
- intel_npu_server-0.3.0/npu_server/generation/engine.py +206 -0
- intel_npu_server-0.3.0/npu_server/generation/events.py +34 -0
- intel_npu_server-0.3.0/npu_server/generation/streaming.py +85 -0
- intel_npu_server-0.3.0/npu_server/load_model/__init__.py +3 -0
- intel_npu_server-0.3.0/npu_server/load_model/loader.py +116 -0
- intel_npu_server-0.3.0/npu_server/load_model/preparer.py +209 -0
- intel_npu_server-0.3.0/npu_server/load_model/runtime.py +84 -0
- intel_npu_server-0.3.0/npu_server/server/__init__.py +3 -0
- intel_npu_server-0.3.0/npu_server/server/http_app.py +200 -0
- intel_npu_server-0.3.0/npu_server/server/message_normalizer.py +83 -0
- intel_npu_server-0.3.0/npu_server/server/openai_schemas.py +65 -0
- intel_npu_server-0.3.0/npu_server/tool_call_parsers.py +170 -0
- intel_npu_server-0.3.0/pyproject.toml +64 -0
- intel_npu_server-0.3.0/pyproject.toml.orig +60 -0
|
@@ -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,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
|