mirror of
https://github.com/deepseek-ai/deepseek-recipe.git
synced 2026-10-26 16:55:05 +00:00
Co-authored-by: xinchengxx <xinchengxx@deepseek.com> Co-authored-by: zhu-he <hezhu@deepseek.com>
234 lines
9.9 KiB
Markdown
234 lines
9.9 KiB
Markdown
# Use with tokenizer
|
|
|
|
**English** | [中文](tokenizer.zh.md)
|
|
|
|
With a tokenizer attached, a `PromptEncoding` encodes conversations into model
|
|
token IDs, and a `StreamProcessor` accepts token IDs. Neither loads a tokenizer
|
|
on its own: pass one with `with_tokenizer`, then call the corresponding method.
|
|
Without an attached tokenizer, `encode` returns
|
|
`EncodingError::MissingTokenizer`, and a `Tokens` chunk fails the stream with
|
|
`StreamError::MissingTokenizer`.
|
|
|
|
Use the tokenizer that matches your model. This repository bundles copies under
|
|
[`static/tokenizers/`](../static/tokenizers/README.md) for the V4 and V4.1
|
|
prompt encodings; load `tokenizer.json` from the matching directory. You can
|
|
also load a tokenizer from the Hugging Face Hub. Its added vocabulary must
|
|
contain the spelling of every special token that rendering writes; otherwise
|
|
`encode` returns `EncodingError::Encode`. `IMAGE_SPECIAL_TOKEN_ID` is the token
|
|
ID of the image token in the bundled V4.1 tokenizer; a caller that uses this
|
|
constant needs a tokenizer that assigns the image token the same token ID.
|
|
|
|
## Rust
|
|
|
|
Add the dependencies to your application:
|
|
|
|
```sh
|
|
cargo add deepseek-recipe@0.2 deepseek-recipe-encoding@0.2 serde_json@1 tokenizers@0.23
|
|
```
|
|
|
|
Save this as `src/main.rs`. The tokenizer path is relative to the working
|
|
directory, so run the application from this repository's root with
|
|
`cargo run --manifest-path path/to/your-app/Cargo.toml`; elsewhere, use an
|
|
absolute path or your own `tokenizer.json`:
|
|
|
|
```rust
|
|
use deepseek_recipe::openai::ChatCompletionRequest;
|
|
use deepseek_recipe::request::{ConversionOptions, ProtocolRequest};
|
|
use deepseek_recipe_encoding::{PromptEncoding, PromptTokenizer};
|
|
use deepseek_recipe_encoding::v4::dsv41::DeepseekV41Encoding;
|
|
use serde_json::json;
|
|
use tokenizers::Tokenizer;
|
|
|
|
fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
let request: ChatCompletionRequest = serde_json::from_value(json!({
|
|
"model": "DeepSeek-V4.1-Flash",
|
|
"messages": [{"role": "user", "content": "Hello"}],
|
|
}))?;
|
|
let converted = request.convert(ConversionOptions::default())?;
|
|
|
|
// Attach the tokenizer, then encode the conversation into token IDs.
|
|
let tokenizer = Tokenizer::from_file("static/tokenizers/v41/tokenizer.json")?;
|
|
let encoding = DeepseekV41Encoding::new().with_tokenizer(PromptTokenizer::new(&tokenizer));
|
|
let token_ids = encoding.encode(&converted.conversation)?;
|
|
|
|
println!("{token_ids:?}");
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
Pass `with_tokenizer` a `PromptTokenizer`, which prepares a `Tokenizer` by
|
|
keeping a copy without the added vocabulary, or attach any other
|
|
`TokenizerEncoder` implementation. `encode` renders the conversation and then
|
|
tokenizes the prompt.
|
|
|
|
`render_conversation` performs only the rendering step: it returns the prompt
|
|
text, the byte ranges of its explicit special tokens (`special_ranges`), and its
|
|
image sources. Rendering inserts the template's control tokens, the placeholders
|
|
of a message's image sources, and the grounding tokens in assistant content and
|
|
reasoning content as explicit special tokens. During tokenization, each special
|
|
range encodes as the token ID of its spelling, and the text between special
|
|
ranges is plain text, encoded without the added vocabulary. `PromptTokenizer`
|
|
encodes the plain regions as one batch, which the `tokenizers` crate can encode
|
|
in parallel, depending on its `TOKENIZERS_PARALLELISM` setting.
|
|
|
|
A special-token spelling typed into a user message is plain text, so it cannot
|
|
act as a control token. A caller that tokenizes `RenderedPrompt::prompt` itself
|
|
must apply `special_ranges` the same way.
|
|
|
|
After encoding, the caller resolves the `max_tokens` to send with
|
|
`ConversationRequest::resolve_max_tokens(prompt_tokens, context_limit)`.
|
|
`prompt_tokens` is the number of encoded token IDs plus the image token counts
|
|
beyond one per placeholder (`MultiModalData::image_token_adjustment`), and
|
|
`context_limit` is the number of tokens the model's context holds. A requested
|
|
`max_tokens` is kept. An unspecified one becomes 8192 outside thinking mode,
|
|
131072 in thinking mode with `max` reasoning effort, and 65536 in thinking mode
|
|
otherwise, lowered to the context left after the prompt. The method returns a
|
|
`ConversionError` when the prompt and the requested `max_tokens` together
|
|
exceed `context_limit`.
|
|
|
|
### Decoding
|
|
|
|
A backend can produce token IDs instead of text. Attach the same tokenizer to
|
|
the stream processor to decode those chunks. The example also needs
|
|
`tokio-stream`, plus `tokio` with the `macros` and `rt-multi-thread` features,
|
|
as listed in [response streaming](streaming.md). The tokenizer path has the same
|
|
requirement as in the encoding example:
|
|
|
|
```rust
|
|
use deepseek_recipe::openai::ChatCompletionRequest;
|
|
use deepseek_recipe::request::{ConversionOptions, ProtocolRequest};
|
|
use deepseek_recipe::stream::{InferenceChunk, InferenceFinishReason, StreamProcessor};
|
|
use serde_json::json;
|
|
use tokenizers::Tokenizer;
|
|
use tokio_stream::{StreamExt, iter};
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
let request: ChatCompletionRequest = serde_json::from_value(json!({
|
|
"model": "DeepSeek-V4.1-Flash",
|
|
"messages": [{"role": "user", "content": "Hello"}],
|
|
"thinking": {"type": "disabled"},
|
|
}))?;
|
|
let converted = request.convert(ConversionOptions::default())?;
|
|
let generator = ChatCompletionRequest::chunk_generator(
|
|
&converted,
|
|
"id-1".into(),
|
|
"DeepSeek-V4.1-Flash".into(),
|
|
);
|
|
|
|
// The backend answers with token IDs; the attached tokenizer decodes them.
|
|
let tokenizer = Tokenizer::from_file("static/tokenizers/v41/tokenizer.json")?;
|
|
let token_ids = tokenizer.encode("Hello there!", false)?.get_ids().to_vec();
|
|
let processor =
|
|
StreamProcessor::new(generator, converted.parsing_options).with_tokenizer(tokenizer);
|
|
|
|
let inference = iter([
|
|
InferenceChunk::Tokens { token_ids, logprobs: None },
|
|
InferenceChunk::Finish { finish_reason: InferenceFinishReason::Stop },
|
|
]);
|
|
|
|
let mut chunks = std::pin::pin!(processor.into_protocol_stream(inference));
|
|
while let Some(chunk) = chunks.next().await {
|
|
println!("{}", serde_json::to_string(&chunk?)?);
|
|
}
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
The processor decodes token IDs incrementally and buffers them while a
|
|
multi-token character is incomplete; token IDs that produced text count as
|
|
completion tokens. Each special token is decoded as its own piece and marked, so
|
|
the output parser recognizes `</think>` and the DSML tags only as tokens; the
|
|
same text spelled with plain tokens is content. Text chunks carry no token
|
|
boundaries, so their markers match by spelling. A `Tokens` chunk without an
|
|
attached tokenizer fails the stream with `StreamError::MissingTokenizer`.
|
|
`Tokenizer` implements the `StreamTokenizer` trait used by `with_tokenizer`.
|
|
|
|
`InferenceChunk::Tokens` carries the token IDs of one backend output, which the
|
|
processor decodes together. When the converted request sets
|
|
`InferenceOptions::logprobs`, the caller supplies one `InferenceTokenLogprob`
|
|
per token ID: the decoded text then carries the log probabilities of its
|
|
tokens, and Chat Completions and Responses report them. A `Tokens` chunk whose log
|
|
probability count differs from its token ID count fails the stream with
|
|
`StreamError::LogprobCountMismatch`. In thinking mode, the processor also uses
|
|
the attached tokenizer to count `reasoning_tokens` for usage by encoding the
|
|
emitted reasoning content, for text chunks as well as token chunks. Without a
|
|
tokenizer, Chat Completions usage omits `completion_tokens_details`, and
|
|
Responses usage reports `reasoning_tokens` as `0`.
|
|
|
|
## Python
|
|
|
|
Install the package with `python3 -m pip install deepseek-recipe`. The tokenizer
|
|
path has the same requirement as in the Rust example:
|
|
|
|
```python
|
|
from deepseek_recipe import (
|
|
ChatCompletionRequest,
|
|
ConversionOptions,
|
|
DeepseekV41Encoding,
|
|
Tokenizer,
|
|
)
|
|
|
|
request = ChatCompletionRequest({
|
|
"model": "DeepSeek-V4.1-Flash",
|
|
"messages": [{"role": "user", "content": "Hello"}],
|
|
})
|
|
converted = request.convert(ConversionOptions())
|
|
|
|
tokenizer = Tokenizer.from_file("static/tokenizers/v41/tokenizer.json")
|
|
encoding = DeepseekV41Encoding().with_tokenizer(tokenizer)
|
|
print(encoding.encode(converted.conversation))
|
|
```
|
|
|
|
`Tokenizer.from_pretrained` downloads and caches a `tokenizer.json` from the
|
|
Hugging Face Hub, and `Tokenizer.from_str` loads the file contents directly.
|
|
`render_conversation` and `encode` return the same results as in Rust; `encode`
|
|
raises `RuntimeError` when no tokenizer is attached or encoding fails. After
|
|
encoding, `ConversationRequest.resolve_max_tokens` resolves `max_tokens` as in
|
|
Rust and raises `ConversionError` where Rust returns one.
|
|
|
|
### Decoding
|
|
|
|
Pass the tokenizer as the processor's third argument. A `tokens` chunk is
|
|
decoded with the same behavior as in Rust; without a tokenizer, such a chunk
|
|
raises `RuntimeError` and closes the processor.
|
|
|
|
```python
|
|
from deepseek_recipe import (
|
|
ChatCompletionRequest,
|
|
ConversionOptions,
|
|
InferenceChunk,
|
|
InferenceFinishReason,
|
|
StreamProcessor,
|
|
Tokenizer,
|
|
)
|
|
|
|
converted = ChatCompletionRequest({
|
|
"model": "DeepSeek-V4.1-Flash",
|
|
"messages": [{"role": "user", "content": "Hello"}],
|
|
"thinking": {"type": "disabled"},
|
|
}).convert(ConversionOptions())
|
|
generator = ChatCompletionRequest.chunk_generator(converted, "id-1", "DeepSeek-V4.1-Flash")
|
|
|
|
# The backend answers with token IDs; the tokenizer passed to the processor
|
|
# decodes them.
|
|
tokenizer = Tokenizer.from_file("static/tokenizers/v41/tokenizer.json")
|
|
token_ids = tokenizer.encode("Hello there!")
|
|
processor = StreamProcessor(generator, converted.parsing_options, tokenizer)
|
|
|
|
chunks = []
|
|
for chunk in (
|
|
InferenceChunk.tokens(token_ids),
|
|
InferenceChunk.finish(InferenceFinishReason.Stop),
|
|
):
|
|
chunks.extend(processor.push(chunk))
|
|
chunks.extend(processor.finish())
|
|
|
|
for chunk in chunks:
|
|
print(chunk.to_json())
|
|
processor.close()
|
|
```
|
|
|
|
See the [project README](../README.md) for supported scope and the
|
|
[response streaming](streaming.md) guide for the full chunk sequence.
|