mirror of
https://github.com/deepseek-ai/deepseek-recipe.git
synced 2026-10-26 16:55:05 +00:00
Release 0.2.0
Co-authored-by: xinchengxx <xinchengxx@deepseek.com> Co-authored-by: zhu-he <hezhu@deepseek.com>
This commit is contained in:
1 parent
8cadfede70
commit
93b0fd229c
111 files changed
+15543
-4797
No files matched your search
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "encoding-decoding-demo"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description = "A local web interface for inspecting DeepSeek prompts and decoding model output."
|
||||
description = "A local web interface for inspecting DeepSeek prompts and parsing model output."
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# Encoding & Decoding Demo
|
||||
|
||||
A web interface for rendering DeepSeek V4.1 prompts, inspecting special tokens,
|
||||
and decoding complete model output into Chat Completions, Responses, and Messages
|
||||
API formats.
|
||||
A web interface for rendering DeepSeek V4 and V4.1 prompts, inspecting special
|
||||
tokens, and parsing complete model output into Chat Completions, Responses, and
|
||||
Messages responses. The demo renders prompts and parses model output; it does
|
||||
not encode or decode token IDs.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -14,54 +15,61 @@ repository root:
|
||||
cargo run -p encoding-decoding-demo --locked
|
||||
```
|
||||
|
||||
Open [http://127.0.0.1:7778](http://127.0.0.1:7778). Set `DEMO_ADDR` to change the
|
||||
listening address.
|
||||
Open [http://127.0.0.1:7778](http://127.0.0.1:7778). Set `DEMO_ADDR` to change
|
||||
the listening address.
|
||||
|
||||
Use the Encode / Decode buttons to switch between modes; only the selected
|
||||
mode's editor and result are shown. In Encode mode, select an API format, edit
|
||||
its request JSON, and render the request to inspect the model prompt. In Decode
|
||||
mode, edit the complete assistant output and decode it. Model output is
|
||||
rendered with the same special-token highlights as the prompt. Switching modes or
|
||||
formats keeps each request draft and reuses the same model output.
|
||||
Use the Render / Parse buttons to switch between modes; only the selected
|
||||
mode's editor and result are shown. In Render mode, select an API format, edit
|
||||
its request JSON, then render it and inspect the prompt. Render mode shows
|
||||
prompt text; it does not encode the prompt into token IDs. In Parse mode, edit
|
||||
the complete assistant output and parse it into the selected format's protocol
|
||||
response. Model output is displayed with the same special-token highlights as
|
||||
the prompt. Switching modes or formats keeps each request draft and reuses the
|
||||
same model output.
|
||||
|
||||
The request's `model` name selects the prompt encoding. `DeepSeek-V4-Pro` and
|
||||
`deepseek-v4-pro` render V4 prompts; `DeepSeek-V4.1-Flash`, `deepseek-flash`,
|
||||
and `encoding-decoding-demo` render V4.1 prompts. A request without a `model`
|
||||
uses `encoding-decoding-demo`. Any other name is rejected with HTTP 400.
|
||||
|
||||
The example output includes `<think>…</think>` reasoning, answer text, two DSML
|
||||
weather tool calls (Beijing and Shanghai, each with string and numeric
|
||||
arguments), and the end-of-turn marker `<|end▁of▁sentence|>`. Each protocol's
|
||||
sample request declares the weather tool and includes the preceding tool
|
||||
result and follow-up question.
|
||||
arguments), and the end-of-sentence token (EOS) `<|end▁of▁sentence|>`. Each
|
||||
protocol's sample request declares the weather tool and includes the preceding
|
||||
tool result and follow-up question.
|
||||
|
||||
The decoded JSON view shows only response content, reasoning, and tool calls.
|
||||
It omits finish status and server-generated metadata such as response
|
||||
and tool-call IDs, model names, timestamps, token usage, and thinking
|
||||
The protocol response view shows only answer content, reasoning content, and
|
||||
tool calls. It omits finish status and server-generated metadata such as
|
||||
response and tool-call IDs, model names, timestamps, token usage, and thinking
|
||||
signatures. Tool inputs and arguments remain intact, including payload fields
|
||||
with those names. The HTTP endpoint still returns the complete protocol
|
||||
with those names. The HTTP endpoint responds with the complete protocol
|
||||
response.
|
||||
|
||||
The decoder accepts complete assistant content, including the opening
|
||||
`<think>` when reasoning is present, and an optional leading `<|Assistant|>`.
|
||||
It consumes these frame markers before passing the content to the library's
|
||||
stream parser. Output without an opening `<think>` is answer content; a
|
||||
leading `</think>` from a non-thinking assistant prefix is also accepted.
|
||||
Parse mode accepts complete assistant output, including the opening `<think>`
|
||||
when reasoning is present, and an optional leading `<|Assistant|>`. It strips
|
||||
these leading tokens before passing the content to the library's stream parser.
|
||||
Output without an opening `<think>` is answer content; a leading `</think>`,
|
||||
which the prompt ends with outside thinking mode, is also accepted.
|
||||
Parsing starts outside tool-call markup because the complete output includes
|
||||
its DSML opening block. Tool declarations, JSON output mode, and stop sequences
|
||||
come from the selected protocol request.
|
||||
|
||||
Edit the model output, then use the adjacent decode button or Cmd/Ctrl+Enter. The UI
|
||||
uses the default `stop` finish reason without displaying protocol status. If
|
||||
the pasted output contains `<|end▁of▁sentence|>`, the demo treats its first
|
||||
occurrence as EOS and discards that marker and everything after it.
|
||||
Edit the model output, then use the adjacent Parse button or Cmd/Ctrl+Enter.
|
||||
The UI uses the default `stop` finish reason without displaying protocol status.
|
||||
If the pasted output contains the EOS, the demo treats its first occurrence as
|
||||
the end of the output and discards that EOS and everything after it.
|
||||
|
||||
## HTTP endpoints
|
||||
|
||||
`POST /api/render` accepts `{ "format": "…", "body": { … } }` and returns the
|
||||
prompt with highlighted segments.
|
||||
`POST /api/render` accepts `{ "format": "…", "body": { … } }` and responds with
|
||||
the rendered prompt and its highlighted segments.
|
||||
|
||||
`POST /api/decode` accepts the same request plus `output` and an optional
|
||||
`POST /api/parse` accepts the same request plus `output` and an optional
|
||||
`finish_reason` (default: `stop`). The formats are `chat_completions`,
|
||||
`responses`, and `messages`. For example:
|
||||
|
||||
```sh
|
||||
curl http://127.0.0.1:7778/api/decode \
|
||||
curl http://127.0.0.1:7778/api/parse \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{
|
||||
"format": "chat_completions",
|
||||
@@ -72,16 +80,18 @@ curl http://127.0.0.1:7778/api/decode \
|
||||
```
|
||||
|
||||
The result is `{ "response": { … }, "segments": [ … ] }`. `segments` preserves
|
||||
the complete supplied model output for highlighting, including its frame
|
||||
markers. `response` is the complete protocol response, even when the input
|
||||
request sets `stream: true`. The demo
|
||||
uses the supplied model name or `encoding-decoding-demo`, a demonstration response ID,
|
||||
and zero token usage because no tokenizer or backend usage data is supplied.
|
||||
Decoding uses the library's stream parser and protocol response accumulators.
|
||||
API callers can supply `length` or `content_filter` as the backend finish
|
||||
reason. EOS uses `stop` instead, and matched request stop sequences take
|
||||
the complete supplied model output for highlighting, including the leading
|
||||
`<|Assistant|>` and `<think>`. `response` is the complete protocol response,
|
||||
even when the input request sets `stream: true`. The demo uses the supplied
|
||||
model name or `encoding-decoding-demo`, a demonstration response ID, and zero
|
||||
token usage because no tokenizer or backend usage data is supplied.
|
||||
The endpoint parses the output with the library's `StreamProcessor` and
|
||||
accumulates the protocol chunks into the complete response. API callers can
|
||||
supply `length` or `content_filter` as the backend finish reason. Output that
|
||||
contains the EOS uses `stop` instead, and matched request stop sequences take
|
||||
precedence.
|
||||
|
||||
This example has no authentication. Keep the default loopback address for local
|
||||
use. It renders prompts and decodes supplied text; it does not execute
|
||||
inference, run tools, or fetch image URLs.
|
||||
use. It renders prompts and parses model output; it does not encode
|
||||
conversations into token IDs, decode token IDs, execute inference, run tools, or
|
||||
fetch image URLs.
|
||||
@@ -11,21 +11,16 @@ use deepseek_recipe::anthropic::MessagesRequest;
|
||||
use deepseek_recipe::error_response::ErrorResponse;
|
||||
use deepseek_recipe::openai::{ChatCompletionRequest, ResponsesRequest};
|
||||
use deepseek_recipe::request::{
|
||||
ConversationRequest, ConversionError, ConversionOptions, ProtocolRequest,
|
||||
ConversationRequest, ConversionError, ConversionOptions, ModelVersion, ProtocolRequest,
|
||||
};
|
||||
use deepseek_recipe::response::ProtocolResponse;
|
||||
use deepseek_recipe::stream::state_machine::ReasoningStage;
|
||||
use deepseek_recipe::response::{Accumulate, ProtocolResponse};
|
||||
use deepseek_recipe::stream::{
|
||||
ChunkGenerator, InferenceChunk, InferenceFinishReason, StreamProcessor,
|
||||
ChunkGenerator, InferenceChunk, InferenceFinishReason, StreamProcessor, ThinkingInitialStage,
|
||||
};
|
||||
use deepseek_recipe::util::append_delta::AppendDelta;
|
||||
use deepseek_recipe_core::multimodal::IMAGE_SPECIAL_TOKEN;
|
||||
use deepseek_recipe_core::special_tokens::SpecialToken;
|
||||
use deepseek_recipe_encoding::PromptEncoding;
|
||||
use deepseek_recipe_encoding::v4::dsv4::DeepseekV4Encoding;
|
||||
use deepseek_recipe_encoding::v4::dsv41::DeepseekV41Encoding;
|
||||
use deepseek_recipe_encoding::v4::{
|
||||
ASSISTANT_SP_TOKEN, BOS_TOKEN, DSML_SP_TOKEN, EOS_TOKEN, LATEST_REMINDER_SP_TOKEN,
|
||||
SYSTEM_SP_TOKEN, THINKING_END_TOKEN, THINKING_START_TOKEN, USER_SP_TOKEN,
|
||||
};
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
@@ -67,47 +62,47 @@ impl Highlight {
|
||||
const HIGHLIGHTS: &[Highlight] = &[
|
||||
Highlight::new(
|
||||
"bos",
|
||||
&[BOS_TOKEN],
|
||||
"Begin of sentence. Marks the start of the prompt.",
|
||||
&[SpecialToken::Bos.default_str()],
|
||||
"Beginning-of-sentence token (BOS). Marks the start of the prompt.",
|
||||
),
|
||||
Highlight::new(
|
||||
"system",
|
||||
&[SYSTEM_SP_TOKEN],
|
||||
&[SpecialToken::System.default_str()],
|
||||
"System turn marker. Starts a system message.",
|
||||
),
|
||||
Highlight::new(
|
||||
"user",
|
||||
&[USER_SP_TOKEN],
|
||||
&[SpecialToken::User.default_str()],
|
||||
"User turn marker. Starts a user message.",
|
||||
),
|
||||
Highlight::new(
|
||||
"assistant",
|
||||
&[ASSISTANT_SP_TOKEN],
|
||||
&[SpecialToken::Assistant.default_str()],
|
||||
"Assistant turn marker. Starts an assistant message.",
|
||||
),
|
||||
Highlight::new(
|
||||
"thinking_start",
|
||||
&[THINKING_START_TOKEN],
|
||||
&[SpecialToken::ThinkingStart.default_str()],
|
||||
"Starts the reasoning content.",
|
||||
),
|
||||
Highlight::new(
|
||||
"thinking_end",
|
||||
&[THINKING_END_TOKEN],
|
||||
&[SpecialToken::ThinkingEnd.default_str()],
|
||||
"Ends the reasoning content.",
|
||||
),
|
||||
Highlight::new(
|
||||
"latest_reminder",
|
||||
&[LATEST_REMINDER_SP_TOKEN],
|
||||
&[SpecialToken::LatestReminder.default_str()],
|
||||
"Starts the latest reminder message.",
|
||||
),
|
||||
Highlight::new(
|
||||
"eos",
|
||||
&[EOS_TOKEN],
|
||||
"End of sentence. Terminates the message.",
|
||||
&[SpecialToken::Eos.default_str()],
|
||||
"End-of-sentence token (EOS). Ends an assistant message.",
|
||||
),
|
||||
Highlight::new(
|
||||
"dsml",
|
||||
&[DSML_SP_TOKEN],
|
||||
&[SpecialToken::Dsml.default_str()],
|
||||
"DeepSeek markup tag. Structures tool calls in the prompt.",
|
||||
),
|
||||
Highlight::new(
|
||||
@@ -117,7 +112,7 @@ const HIGHLIGHTS: &[Highlight] = &[
|
||||
),
|
||||
Highlight::new(
|
||||
"image",
|
||||
&[IMAGE_SPECIAL_TOKEN],
|
||||
&[SpecialToken::Image.default_str()],
|
||||
"Marks one image in the prompt.",
|
||||
),
|
||||
Highlight::new(
|
||||
@@ -219,34 +214,34 @@ struct RenderResponse {
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum DecodeFinishReason {
|
||||
enum ParseFinishReason {
|
||||
#[default]
|
||||
Stop,
|
||||
Length,
|
||||
ContentFilter,
|
||||
}
|
||||
|
||||
impl From<DecodeFinishReason> for InferenceFinishReason {
|
||||
fn from(reason: DecodeFinishReason) -> Self {
|
||||
impl From<ParseFinishReason> for InferenceFinishReason {
|
||||
fn from(reason: ParseFinishReason) -> Self {
|
||||
match reason {
|
||||
DecodeFinishReason::Stop => Self::Stop,
|
||||
DecodeFinishReason::Length => Self::Length,
|
||||
DecodeFinishReason::ContentFilter => Self::ContentFilter,
|
||||
ParseFinishReason::Stop => Self::Stop,
|
||||
ParseFinishReason::Length => Self::Length,
|
||||
ParseFinishReason::ContentFilter => Self::ContentFilter,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct DecodeRequest {
|
||||
struct ParseRequest {
|
||||
format: ApiFormat,
|
||||
body: serde_json::Value,
|
||||
output: String,
|
||||
#[serde(default)]
|
||||
finish_reason: DecodeFinishReason,
|
||||
finish_reason: ParseFinishReason,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct DecodeResponse {
|
||||
struct ParseResponse {
|
||||
response: serde_json::Value,
|
||||
segments: Vec<Segment>,
|
||||
}
|
||||
@@ -260,33 +255,50 @@ fn bad_request(detail: String) -> DemoError {
|
||||
fn conversion_failed(error: ConversionError) -> DemoError {
|
||||
let status =
|
||||
StatusCode::from_u16(error.status_code()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
(status, Json(error.into_response()))
|
||||
(status, Json(error.into_error_response()))
|
||||
}
|
||||
|
||||
fn convert_request<T>(mut body: serde_json::Value) -> Result<ConversationRequest, DemoError>
|
||||
/// Convert a protocol request, returning it alongside the requested model name
|
||||
/// echoed into responses.
|
||||
fn convert_request<T>(
|
||||
mut body: serde_json::Value,
|
||||
) -> Result<(ConversationRequest, String), DemoError>
|
||||
where
|
||||
T: ProtocolRequest + DeserializeOwned,
|
||||
{
|
||||
// Requests use V4.1 prompt rendering and a default model name.
|
||||
// A request without a model name uses the default name, which targets V4.1.
|
||||
const DEFAULT_MODEL: &str = "encoding-decoding-demo";
|
||||
if let Some(body) = body.as_object_mut() {
|
||||
body.entry("model")
|
||||
.or_insert_with(|| "encoding-decoding-demo".into());
|
||||
body.entry("model").or_insert_with(|| DEFAULT_MODEL.into());
|
||||
}
|
||||
let model = body["model"].as_str().unwrap_or(DEFAULT_MODEL).to_owned();
|
||||
let request: T = serde_json::from_value(body)
|
||||
.map_err(|err| bad_request(format!("invalid request body: {err}")))?;
|
||||
request
|
||||
.convert(ConversionOptions::default())
|
||||
.map_err(conversion_failed)
|
||||
// The demo also accepts the DeepSeek API model names.
|
||||
let converted = request
|
||||
.convert(
|
||||
ConversionOptions::default()
|
||||
.with_model_alias(DEFAULT_MODEL, ModelVersion::DeepseekV41Flash)
|
||||
.with_model_alias("deepseek-v4-pro", ModelVersion::DeepseekV4Pro)
|
||||
.with_model_alias("deepseek-flash", ModelVersion::DeepseekV41Flash),
|
||||
)
|
||||
.map_err(conversion_failed)?;
|
||||
Ok((converted, model))
|
||||
}
|
||||
|
||||
fn render_prompt<T>(body: serde_json::Value) -> Result<String, DemoError>
|
||||
where
|
||||
T: ProtocolRequest + DeserializeOwned,
|
||||
{
|
||||
let request = convert_request::<T>(body)?;
|
||||
Ok(DeepseekV41Encoding::new()
|
||||
.render_conversation(&request.conversation)
|
||||
.prompt)
|
||||
let (request, _) = convert_request::<T>(body)?;
|
||||
// The model version of the request selects the prompt encoding.
|
||||
let rendered = match request.model_version {
|
||||
ModelVersion::DeepseekV4Pro => {
|
||||
DeepseekV4Encoding::new().render_conversation(&request.conversation)
|
||||
}
|
||||
_ => DeepseekV41Encoding::new().render_conversation(&request.conversation),
|
||||
};
|
||||
Ok(rendered.prompt)
|
||||
}
|
||||
|
||||
async fn render_handler(
|
||||
@@ -301,36 +313,38 @@ async fn render_handler(
|
||||
Ok(Json(RenderResponse { prompt, segments }))
|
||||
}
|
||||
|
||||
async fn decode_output<T>(request: DecodeRequest) -> Result<serde_json::Value, DemoError>
|
||||
async fn parse_output<T>(request: ParseRequest) -> Result<serde_json::Value, DemoError>
|
||||
where
|
||||
T: ProtocolRequest + DeserializeOwned,
|
||||
<<T::Response as ProtocolResponse>::ChunkGenerator as ChunkGenerator>::Chunk: Send,
|
||||
{
|
||||
let mut converted = convert_request::<T>(request.body)?;
|
||||
let (mut converted, model) = convert_request::<T>(request.body)?;
|
||||
// This endpoint always accumulates a complete response, including usage,
|
||||
// even when the input request originally selected streaming transport.
|
||||
converted.stream = false;
|
||||
let (output, finish_reason) = match request.output.split_once(EOS_TOKEN) {
|
||||
let (output, finish_reason) = match request.output.split_once(SpecialToken::Eos.default_str()) {
|
||||
Some((output, _)) => (output, InferenceFinishReason::Stop),
|
||||
None => (request.output.as_str(), request.finish_reason.into()),
|
||||
};
|
||||
// The demo accepts complete assistant output. Consume its leading frame
|
||||
// markers here; the stream parser normally starts after a prompt prefill.
|
||||
let output = output.strip_prefix(ASSISTANT_SP_TOKEN).unwrap_or(output);
|
||||
let (thinking, output) = match output.strip_prefix(THINKING_START_TOKEN) {
|
||||
// The stream parser expects output that starts after the prompt's
|
||||
// assistant-turn start (`<|Assistant|><think>` or `</think>`). The demo
|
||||
// accepts complete assistant output, so it strips that start here.
|
||||
let output = output
|
||||
.strip_prefix(SpecialToken::Assistant.default_str())
|
||||
.unwrap_or(output);
|
||||
let (thinking, output) = match output.strip_prefix(SpecialToken::ThinkingStart.default_str()) {
|
||||
Some(output) => (true, output),
|
||||
None => (
|
||||
false,
|
||||
output.strip_prefix(THINKING_END_TOKEN).unwrap_or(output),
|
||||
output
|
||||
.strip_prefix(SpecialToken::ThinkingEnd.default_str())
|
||||
.unwrap_or(output),
|
||||
),
|
||||
};
|
||||
converted.conversation.thinking_mode = thinking;
|
||||
converted.parsing_options.reasoning_initial_stage = thinking.then_some(ReasoningStage::Start);
|
||||
converted.parsing_options.tool_call_initial_stage = false;
|
||||
let model = converted
|
||||
.model
|
||||
.clone()
|
||||
.unwrap_or_else(|| "encoding-decoding-demo".into());
|
||||
converted.parsing_options.thinking_initial_stage =
|
||||
thinking.then_some(ThinkingInitialStage::ReasoningStart);
|
||||
converted.parsing_options.starts_in_tool_calls = false;
|
||||
let id = "demo-id".to_owned();
|
||||
let generator = T::chunk_generator(&converted, id.clone(), model.clone());
|
||||
let processor = StreamProcessor::new(generator, converted.parsing_options);
|
||||
@@ -338,7 +352,7 @@ where
|
||||
InferenceChunk::Text {
|
||||
content: output.to_owned(),
|
||||
// Raw text alone does not supply tokenizer or backend usage data.
|
||||
content_tokens: 0,
|
||||
token_count: 0,
|
||||
},
|
||||
InferenceChunk::Finish { finish_reason },
|
||||
]);
|
||||
@@ -347,10 +361,10 @@ where
|
||||
.map(|elapsed| elapsed.as_secs())
|
||||
.unwrap_or(0);
|
||||
let mut response = T::Response::new(id, model, created, 0, 0);
|
||||
let mut chunks = pin!(processor.process(inference));
|
||||
let mut chunks = pin!(processor.into_protocol_stream(inference));
|
||||
while let Some(chunk) = chunks.next().await {
|
||||
response
|
||||
.append(chunk.map_err(|error| {
|
||||
.accumulate(chunk.map_err(|error| {
|
||||
conversion_failed(ConversionError::internal(error.to_string()))
|
||||
})?);
|
||||
}
|
||||
@@ -358,16 +372,16 @@ where
|
||||
.map_err(|error| conversion_failed(ConversionError::internal(error.to_string())))
|
||||
}
|
||||
|
||||
async fn decode_handler(
|
||||
Json(request): Json<DecodeRequest>,
|
||||
) -> Result<Json<DecodeResponse>, DemoError> {
|
||||
async fn parse_handler(
|
||||
Json(request): Json<ParseRequest>,
|
||||
) -> Result<Json<ParseResponse>, DemoError> {
|
||||
let segments = segment(&request.output);
|
||||
let response = match request.format {
|
||||
ApiFormat::ChatCompletions => decode_output::<ChatCompletionRequest>(request).await?,
|
||||
ApiFormat::Responses => decode_output::<ResponsesRequest>(request).await?,
|
||||
ApiFormat::Messages => decode_output::<MessagesRequest>(request).await?,
|
||||
ApiFormat::ChatCompletions => parse_output::<ChatCompletionRequest>(request).await?,
|
||||
ApiFormat::Responses => parse_output::<ResponsesRequest>(request).await?,
|
||||
ApiFormat::Messages => parse_output::<MessagesRequest>(request).await?,
|
||||
};
|
||||
Ok(Json(DecodeResponse { response, segments }))
|
||||
Ok(Json(ParseResponse { response, segments }))
|
||||
}
|
||||
|
||||
fn asset(content_type: &'static str, body: &'static str) -> impl IntoResponse {
|
||||
@@ -409,7 +423,7 @@ async fn main() {
|
||||
get(|| async { binary_asset("image/png", FAVICON_PNG) }),
|
||||
)
|
||||
.route("/api/render", post(render_handler))
|
||||
.route("/api/decode", post(decode_handler));
|
||||
.route("/api/parse", post(parse_handler));
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||
println!("deepseek-recipe demo page: http://{addr}");
|
||||
|
||||
@@ -24,6 +24,7 @@ const SAMPLES = {
|
||||
tool_calls: [
|
||||
{
|
||||
id: "call_1",
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_weather",
|
||||
arguments: JSON.stringify({ location: "Beijing", days: 3 }),
|
||||
@@ -79,7 +80,7 @@ const SAMPLES = {
|
||||
};
|
||||
|
||||
const DEFAULT_FORMAT = "chat_completions";
|
||||
const EOT = "<|end▁of▁sentence|>";
|
||||
const EOS = "<|end▁of▁sentence|>";
|
||||
const OUTPUT_SAMPLE = `<think>The user wants to compare the weather in Beijing and Shanghai for the next three days, so I need forecasts for both cities.
|
||||
</think>
|
||||
I'll check the weather in Beijing and Shanghai for the next three days.
|
||||
@@ -93,7 +94,7 @@ I'll check the weather in Beijing and Shanghai for the next three days.
|
||||
<|DSML| parameter name="location" string="true">Shanghai</|DSML| parameter>
|
||||
<|DSML| parameter name="days" string="false">3</|DSML| parameter>
|
||||
</|DSML| invoke>
|
||||
</|DSML| calls>${EOT}`;
|
||||
</|DSML| calls>${EOS}`;
|
||||
|
||||
const tabs = Array.from(document.querySelectorAll(".tab"));
|
||||
const modeTabs = Array.from(document.querySelectorAll(".mode-tab"));
|
||||
@@ -104,12 +105,12 @@ const errorBox = document.getElementById("error");
|
||||
const tip = document.getElementById("tip");
|
||||
const modelOutput = document.getElementById("model-output");
|
||||
const modelOutputPreview = document.getElementById("model-output-preview");
|
||||
const decodedOutput = document.getElementById("decoded-output");
|
||||
const decodeMeta = document.getElementById("decode-meta");
|
||||
const decodeError = document.getElementById("decode-error");
|
||||
const parsedOutput = document.getElementById("parsed-output");
|
||||
const parseMeta = document.getElementById("parse-meta");
|
||||
const parseError = document.getElementById("parse-error");
|
||||
const responseFormat = document.getElementById("response-format");
|
||||
|
||||
const state = { mode: "encode", format: DEFAULT_FORMAT, drafts: { ...SAMPLES }, renderId: 0, decodeId: 0 };
|
||||
const state = { mode: "render", format: DEFAULT_FORMAT, drafts: { ...SAMPLES }, renderId: 0, parseId: 0 };
|
||||
|
||||
function pretty(value) {
|
||||
return JSON.stringify(value, null, 2);
|
||||
@@ -121,7 +122,7 @@ function pick(value, keys) {
|
||||
|
||||
// Select protocol fields explicitly: tool inputs and argument strings are
|
||||
// payloads, so metadata-like keys inside them must remain untouched.
|
||||
function decodedContent(format, response) {
|
||||
function parsedContent(format, response) {
|
||||
switch (format) {
|
||||
case "chat_completions":
|
||||
return {
|
||||
@@ -157,8 +158,8 @@ function decodedContent(format, response) {
|
||||
}
|
||||
|
||||
function runMode() {
|
||||
if (state.mode === "encode") renderPrompt();
|
||||
else decodeOutput();
|
||||
if (state.mode === "render") renderPrompt();
|
||||
else parseOutput();
|
||||
}
|
||||
|
||||
function setMode(mode) {
|
||||
@@ -169,7 +170,7 @@ function setMode(mode) {
|
||||
tab.setAttribute("aria-selected", String(selected));
|
||||
tab.tabIndex = selected ? 0 : -1;
|
||||
}
|
||||
for (const name of ["encode", "decode"]) {
|
||||
for (const name of ["render", "parse"]) {
|
||||
document.getElementById(`${name}-panel`).hidden = name !== mode;
|
||||
document.getElementById(`${name}-actions`).hidden = name !== mode;
|
||||
}
|
||||
@@ -189,9 +190,9 @@ function setFormat(format) {
|
||||
}
|
||||
input.value = state.drafts[format];
|
||||
state.renderId++;
|
||||
state.decodeId++;
|
||||
state.parseId++;
|
||||
clearPrompt();
|
||||
clearDecoded();
|
||||
clearParsed();
|
||||
hideError();
|
||||
runMode();
|
||||
}
|
||||
@@ -243,30 +244,30 @@ async function post(url, body) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function decodeOutput() {
|
||||
const id = ++state.decodeId;
|
||||
async function parseOutput() {
|
||||
const id = ++state.parseId;
|
||||
const format = state.format;
|
||||
clearDecoded();
|
||||
decodeMeta.textContent = "Decoding…";
|
||||
decodedOutput.setAttribute("aria-busy", "true");
|
||||
clearParsed();
|
||||
parseMeta.textContent = "Parsing…";
|
||||
parsedOutput.setAttribute("aria-busy", "true");
|
||||
try {
|
||||
const body = requestBody();
|
||||
const payload = await post("/api/decode", {
|
||||
const payload = await post("/api/parse", {
|
||||
format,
|
||||
body,
|
||||
output: modelOutput.value,
|
||||
});
|
||||
if (id !== state.decodeId) return;
|
||||
if (id !== state.parseId) return;
|
||||
renderModelOutput(payload.segments);
|
||||
decodedOutput.textContent = pretty(decodedContent(format, payload.response));
|
||||
decodeMeta.textContent = "";
|
||||
parsedOutput.textContent = pretty(parsedContent(format, payload.response));
|
||||
parseMeta.textContent = "";
|
||||
} catch (err) {
|
||||
if (id !== state.decodeId) return;
|
||||
clearDecoded();
|
||||
decodeError.textContent = err.message;
|
||||
decodeError.hidden = false;
|
||||
if (id !== state.parseId) return;
|
||||
clearParsed();
|
||||
parseError.textContent = err.message;
|
||||
parseError.hidden = false;
|
||||
} finally {
|
||||
if (id === state.decodeId) decodedOutput.setAttribute("aria-busy", "false");
|
||||
if (id === state.parseId) parsedOutput.setAttribute("aria-busy", "false");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,17 +278,17 @@ function clearPrompt() {
|
||||
tip.hidden = true;
|
||||
}
|
||||
|
||||
function clearDecoded() {
|
||||
decodedOutput.textContent = "";
|
||||
decodedOutput.setAttribute("aria-busy", "false");
|
||||
decodeMeta.textContent = "";
|
||||
decodeError.hidden = true;
|
||||
function clearParsed() {
|
||||
parsedOutput.textContent = "";
|
||||
parsedOutput.setAttribute("aria-busy", "false");
|
||||
parseMeta.textContent = "";
|
||||
parseError.hidden = true;
|
||||
}
|
||||
|
||||
function invalidateDecoded() {
|
||||
state.decodeId++;
|
||||
clearDecoded();
|
||||
decodeMeta.textContent = "Input changed, ready to decode";
|
||||
function invalidateParsed() {
|
||||
state.parseId++;
|
||||
clearParsed();
|
||||
parseMeta.textContent = "Output changed; parse again";
|
||||
}
|
||||
|
||||
function render(payload) {
|
||||
@@ -296,7 +297,7 @@ function render(payload) {
|
||||
fragment.append(renderSegment(segment));
|
||||
}
|
||||
output.replaceChildren(fragment);
|
||||
meta.textContent = `${payload.prompt.length} chars`;
|
||||
meta.textContent = `${payload.prompt.length} characters`;
|
||||
}
|
||||
|
||||
function syncModelOutputScroll() {
|
||||
@@ -396,20 +397,20 @@ output.addEventListener("mouseout", (event) => {
|
||||
});
|
||||
|
||||
document.getElementById("render-prompt").addEventListener("click", renderPrompt);
|
||||
document.getElementById("decode-output").addEventListener("click", decodeOutput);
|
||||
document.getElementById("parse-output").addEventListener("click", parseOutput);
|
||||
|
||||
input.addEventListener("input", () => {
|
||||
state.drafts[state.format] = input.value;
|
||||
state.renderId++;
|
||||
clearPrompt();
|
||||
hideError();
|
||||
meta.textContent = "Request changed, ready to render";
|
||||
invalidateDecoded();
|
||||
meta.textContent = "Request changed; render again";
|
||||
invalidateParsed();
|
||||
});
|
||||
|
||||
modelOutput.addEventListener("input", () => {
|
||||
renderModelOutput();
|
||||
invalidateDecoded();
|
||||
invalidateParsed();
|
||||
});
|
||||
modelOutput.addEventListener("scroll", syncModelOutputScroll);
|
||||
new ResizeObserver(syncModelOutputScroll).observe(modelOutput);
|
||||
@@ -417,7 +418,7 @@ new ResizeObserver(syncModelOutputScroll).observe(modelOutput);
|
||||
modelOutput.addEventListener("keydown", (event) => {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
decodeOutput();
|
||||
parseOutput();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
<span class="badge">Encoding & Decoding Demo</span>
|
||||
</header>
|
||||
|
||||
<nav class="toolbar" aria-label="Encoding, decoding, and API format">
|
||||
<nav class="toolbar" aria-label="Mode and API format">
|
||||
<div class="mode-tabs" role="tablist" aria-label="Mode">
|
||||
<button id="mode-encode" class="mode-tab active" type="button" data-mode="encode" role="tab" aria-controls="encode-panel" aria-selected="true" tabindex="0">Encode</button>
|
||||
<button id="mode-decode" class="mode-tab" type="button" data-mode="decode" role="tab" aria-controls="decode-panel" aria-selected="false" tabindex="-1">Decode</button>
|
||||
<button id="mode-render" class="mode-tab active" type="button" data-mode="render" role="tab" aria-controls="render-panel" aria-selected="true" tabindex="0">Render</button>
|
||||
<button id="mode-parse" class="mode-tab" type="button" data-mode="parse" role="tab" aria-controls="parse-panel" aria-selected="false" tabindex="-1">Parse</button>
|
||||
</div>
|
||||
<span class="toolbar-divider" aria-hidden="true"></span>
|
||||
<div class="tabs" role="tablist" aria-label="API format">
|
||||
@@ -27,39 +27,39 @@
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<section id="encode-panel" class="mode-panel" role="tabpanel" aria-labelledby="mode-encode">
|
||||
<section id="render-panel" class="mode-panel" role="tabpanel" aria-labelledby="mode-render">
|
||||
<div id="error" class="error" role="alert" hidden></div>
|
||||
<div class="grid">
|
||||
<section class="pane">
|
||||
<header class="pane-header">
|
||||
<div class="pane-heading">
|
||||
<h2><label for="input">Original input</label></h2>
|
||||
<h2><label for="input">Request JSON</label></h2>
|
||||
</div>
|
||||
<div id="encode-actions" class="actions">
|
||||
<button id="render-prompt" class="primary" type="button" title="Encode (⌘/Ctrl + Enter)">Encode</button>
|
||||
<div id="render-actions" class="actions">
|
||||
<button id="render-prompt" class="primary" type="button" title="Render (⌘/Ctrl + Enter)">Render</button>
|
||||
</div>
|
||||
</header>
|
||||
<textarea id="input" spellcheck="false" autocomplete="off"></textarea>
|
||||
</section>
|
||||
<section class="pane">
|
||||
<header class="pane-header">
|
||||
<h2>Encoded result <span id="meta" class="meta"></span></h2>
|
||||
<h2>Rendered prompt <span id="meta" class="meta"></span></h2>
|
||||
</header>
|
||||
<pre id="output" class="prompt"></pre>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="decode-panel" class="mode-panel" role="tabpanel" aria-labelledby="mode-decode" hidden>
|
||||
<div id="decode-error" class="error" role="alert" hidden></div>
|
||||
<section id="parse-panel" class="mode-panel" role="tabpanel" aria-labelledby="mode-parse" hidden>
|
||||
<div id="parse-error" class="error" role="alert" hidden></div>
|
||||
<div class="grid">
|
||||
<section class="pane">
|
||||
<header class="pane-header">
|
||||
<div class="pane-heading">
|
||||
<h2><label for="model-output">Original output</label></h2>
|
||||
<h2><label for="model-output">Model output</label></h2>
|
||||
</div>
|
||||
<div id="decode-actions" class="actions" hidden>
|
||||
<button id="decode-output" class="primary" type="button" title="Decode (⌘/Ctrl + Enter)">Decode</button>
|
||||
<div id="parse-actions" class="actions" hidden>
|
||||
<button id="parse-output" class="primary" type="button" title="Parse (⌘/Ctrl + Enter)">Parse</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="model-editor">
|
||||
@@ -69,9 +69,9 @@
|
||||
</section>
|
||||
<section class="pane">
|
||||
<header class="pane-header">
|
||||
<h2>Decoded result <span id="response-format" class="meta">Chat Completions</span> <span id="decode-meta" class="meta" role="status"></span></h2>
|
||||
<h2>Protocol response <span id="response-format" class="meta">Chat Completions</span> <span id="parse-meta" class="meta" role="status"></span></h2>
|
||||
</header>
|
||||
<pre id="decoded-output" class="prompt response" aria-label="Decoded result"></pre>
|
||||
<pre id="parsed-output" class="prompt response" aria-label="Protocol response"></pre>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in new issue
Block a user