[Feat][Responses API] Support custom tools, encrypted reasoning replay, developer tier and model validation (#38690)
Co-authored-by: Xinyuan Tong <xinyuantong.cs@gmail.com> Co-authored-by: Xinyuan Tong <115166877+JustinTong0323@users.noreply.github.com>
This commit is contained in:
co-authored by
Xinyuan Tong
Xinyuan Tong
parent
6dc7b3421b
commit
925e684a88
@@ -9,8 +9,11 @@ from typing import Optional, Union
|
||||
|
||||
import orjson
|
||||
from openai.types.responses import (
|
||||
ResponseCodeInterpreterToolCall,
|
||||
ResponseOutputItem,
|
||||
ResponseOutputMessage,
|
||||
)
|
||||
from openai.types.responses import ResponseOutputMessage as OpenAIResponseOutputMessage
|
||||
from openai.types.responses import (
|
||||
ResponseOutputText,
|
||||
ResponseReasoningItem,
|
||||
)
|
||||
@@ -43,6 +46,11 @@ from openai_harmony import (
|
||||
from sglang.srt.entrypoints.openai.protocol import (
|
||||
ReasoningEffortTier,
|
||||
ResponseInputOutputItem,
|
||||
ResponseOutputMessage,
|
||||
)
|
||||
from sglang.srt.entrypoints.openai.responses_adapters import (
|
||||
decode_reasoning_state,
|
||||
encode_custom_tool_input,
|
||||
)
|
||||
from sglang.srt.utils import random_uuid
|
||||
|
||||
@@ -163,35 +171,66 @@ def parse_response_input(
|
||||
# text chunk always carries the system→developer text_prefix even if
|
||||
# earlier parts were non-text (image/audio) and got dropped.
|
||||
text_chunks = [
|
||||
c for c in content if c.get("type") in ("text", "input_text")
|
||||
c
|
||||
for c in content
|
||||
if c.get("type") in ("text", "input_text", "output_text")
|
||||
]
|
||||
contents = [
|
||||
TextContent(text=(text_prefix if i == 0 else "") + c.get("text", ""))
|
||||
for i, c in enumerate(text_chunks)
|
||||
]
|
||||
msg = Message.from_role_and_contents(role, contents)
|
||||
elif response_msg["type"] == "function_call_output":
|
||||
if role == "assistant" and response_msg.get("phase") is not None:
|
||||
msg = msg.with_channel(
|
||||
"final" if response_msg["phase"] == "final_answer" else "commentary"
|
||||
)
|
||||
elif response_msg["type"] in ("function_call_output", "custom_tool_call_output"):
|
||||
call_id = response_msg["call_id"]
|
||||
call_response: Optional[ResponseFunctionToolCall] = None
|
||||
for prev_response in reversed(prev_responses):
|
||||
call_response = None
|
||||
for previous in reversed(prev_responses):
|
||||
if not isinstance(previous, dict):
|
||||
previous = previous.model_dump()
|
||||
if (
|
||||
isinstance(prev_response, ResponseFunctionToolCall)
|
||||
and prev_response.call_id == call_id
|
||||
previous.get("type") in ("function_call", "custom_tool_call")
|
||||
and previous.get("call_id") == call_id
|
||||
):
|
||||
call_response = prev_response
|
||||
call_response = previous
|
||||
break
|
||||
if call_response is None:
|
||||
raise ValueError(f"No call message found for {call_id}")
|
||||
output = response_msg.get("output", "")
|
||||
if isinstance(output, list):
|
||||
output = "".join(
|
||||
part.get("text", "") for part in output if isinstance(part, dict)
|
||||
)
|
||||
msg = Message.from_author_and_content(
|
||||
Author.new(Role.TOOL, f"functions.{call_response.name}"),
|
||||
response_msg["output"],
|
||||
Author.new(Role.TOOL, f"functions.{call_response['name']}"),
|
||||
output,
|
||||
)
|
||||
elif response_msg["type"] == "reasoning":
|
||||
content = response_msg["content"]
|
||||
assert len(content) == 1
|
||||
msg = Message.from_role_and_content(Role.ASSISTANT, content[0]["text"])
|
||||
elif response_msg["type"] == "function_call":
|
||||
msg = Message.from_role_and_content(Role.ASSISTANT, response_msg["arguments"])
|
||||
text = ""
|
||||
for field in ("summary", "content"):
|
||||
text = "\n".join(
|
||||
part.get("text", "")
|
||||
for part in response_msg.get(field) or []
|
||||
if isinstance(part, dict)
|
||||
)
|
||||
if text:
|
||||
break
|
||||
if not text:
|
||||
text = decode_reasoning_state(response_msg.get("encrypted_content")) or ""
|
||||
msg = Message.from_role_and_content(Role.ASSISTANT, text).with_channel(
|
||||
"analysis"
|
||||
)
|
||||
elif response_msg["type"] in ("function_call", "custom_tool_call"):
|
||||
arguments = (
|
||||
encode_custom_tool_input(response_msg.get("input") or "")
|
||||
if response_msg["type"] == "custom_tool_call"
|
||||
else response_msg.get("arguments") or "{}"
|
||||
)
|
||||
if isinstance(arguments, dict):
|
||||
arguments = orjson.dumps(arguments).decode()
|
||||
msg = Message.from_role_and_content(Role.ASSISTANT, arguments)
|
||||
msg = msg.with_channel("commentary")
|
||||
msg = msg.with_recipient(f"functions.{response_msg['name']}")
|
||||
msg = msg.with_content_type("json")
|
||||
@@ -201,10 +240,13 @@ def parse_response_input(
|
||||
|
||||
|
||||
def parse_response_output(output: ResponseOutputItem) -> Message:
|
||||
if isinstance(output, ResponseOutputMessage):
|
||||
if isinstance(output, OpenAIResponseOutputMessage):
|
||||
role = output.role
|
||||
contents = [TextContent(text=c.text) for c in output.content]
|
||||
msg = Message.from_role_and_contents(role, contents)
|
||||
phase = getattr(output, "phase", None)
|
||||
if phase is not None:
|
||||
msg = msg.with_channel("final" if phase == "final_answer" else "commentary")
|
||||
return msg
|
||||
elif isinstance(output, ResponseFunctionToolCall):
|
||||
msg = Message.from_role_and_content(Role.ASSISTANT, output.arguments)
|
||||
@@ -282,6 +324,17 @@ def parse_output_message(message: Message):
|
||||
type="web_search_call",
|
||||
)
|
||||
output_items.append(web_search_item)
|
||||
elif recipient is not None and recipient.startswith("python"):
|
||||
output_items.append(
|
||||
ResponseCodeInterpreterToolCall(
|
||||
id=f"ci_{random_uuid()}",
|
||||
type="code_interpreter_call",
|
||||
code="".join(content.text for content in message.content),
|
||||
container_id="auto",
|
||||
outputs=[],
|
||||
status="completed",
|
||||
)
|
||||
)
|
||||
elif message.channel == "analysis":
|
||||
for content in message.content:
|
||||
reasoning_item = ResponseReasoningItem(
|
||||
@@ -296,9 +349,9 @@ def parse_output_message(message: Message):
|
||||
status=None,
|
||||
)
|
||||
output_items.append(reasoning_item)
|
||||
elif message.channel == "commentary":
|
||||
elif message.channel == "commentary" and message.recipient is not None:
|
||||
if message.recipient.startswith("functions."):
|
||||
function_name = message.recipient.split(".")[-1]
|
||||
function_name = message.recipient.removeprefix("functions.")
|
||||
for content in message.content:
|
||||
random_id = random_uuid()
|
||||
response_item = ResponseFunctionToolCall(
|
||||
@@ -327,7 +380,9 @@ def parse_output_message(message: Message):
|
||||
output_items.append(reasoning_item)
|
||||
else:
|
||||
raise ValueError(f"Unknown recipient: {message.recipient}")
|
||||
elif message.channel == "final":
|
||||
elif message.channel == "final" or (
|
||||
message.channel == "commentary" and message.recipient is None
|
||||
):
|
||||
contents = []
|
||||
for content in message.content:
|
||||
output_text = ResponseOutputText(
|
||||
@@ -338,6 +393,7 @@ def parse_output_message(message: Message):
|
||||
)
|
||||
contents.append(output_text)
|
||||
text_item = ResponseOutputMessage(
|
||||
phase="final_answer" if message.channel == "final" else "commentary",
|
||||
id=f"msg_{random_uuid()}",
|
||||
content=contents,
|
||||
role=message.author.role,
|
||||
@@ -359,6 +415,17 @@ def parse_remaining_state(parser: StreamableParser):
|
||||
if current_recipient is not None and current_recipient.startswith("browser."):
|
||||
return []
|
||||
|
||||
if current_recipient is not None and current_recipient.startswith("python"):
|
||||
return [
|
||||
ResponseCodeInterpreterToolCall(
|
||||
id=f"ci_{random_uuid()}",
|
||||
type="code_interpreter_call",
|
||||
code=parser.current_content,
|
||||
container_id="auto",
|
||||
outputs=[],
|
||||
status="in_progress",
|
||||
)
|
||||
]
|
||||
if parser.current_channel == "analysis":
|
||||
reasoning_item = ResponseReasoningItem(
|
||||
id=f"rs_{random_uuid()}",
|
||||
@@ -372,7 +439,21 @@ def parse_remaining_state(parser: StreamableParser):
|
||||
status=None,
|
||||
)
|
||||
return [reasoning_item]
|
||||
elif parser.current_channel == "final":
|
||||
elif current_recipient is not None and current_recipient.startswith("functions."):
|
||||
random_id = random_uuid()
|
||||
return [
|
||||
ResponseFunctionToolCall(
|
||||
id=f"ft_{random_id}",
|
||||
call_id=f"call_{random_id}",
|
||||
type="function_call",
|
||||
name=current_recipient.removeprefix("functions."),
|
||||
arguments=parser.current_content,
|
||||
status="in_progress",
|
||||
)
|
||||
]
|
||||
elif parser.current_channel == "final" or (
|
||||
parser.current_channel == "commentary" and current_recipient is None
|
||||
):
|
||||
output_text = ResponseOutputText(
|
||||
text=parser.current_content,
|
||||
annotations=[], # TODO
|
||||
@@ -380,6 +461,7 @@ def parse_remaining_state(parser: StreamableParser):
|
||||
logprobs=None, # TODO
|
||||
)
|
||||
text_item = ResponseOutputMessage(
|
||||
phase="final_answer" if parser.current_channel == "final" else "commentary",
|
||||
id=f"msg_{random_uuid()}",
|
||||
content=[output_text],
|
||||
role="assistant",
|
||||
|
||||
@@ -39,11 +39,14 @@ from openai.types.responses import (
|
||||
ResponseFunctionToolCall,
|
||||
ResponseInputItemParam,
|
||||
ResponseOutputItem,
|
||||
ResponseOutputMessage,
|
||||
)
|
||||
from openai.types.responses import ResponseOutputMessage as OpenAIResponseOutputMessage
|
||||
from openai.types.responses import (
|
||||
ResponseOutputText,
|
||||
ResponseReasoningItem,
|
||||
ResponseTextConfig,
|
||||
)
|
||||
from openai.types.responses.easy_input_message_param import EasyInputMessageParam
|
||||
from openai.types.responses.response import ToolChoice
|
||||
from openai.types.responses.response_format_text_json_schema_config import (
|
||||
ResponseFormatTextJSONSchemaConfig,
|
||||
@@ -713,6 +716,7 @@ class ChatCompletionMessageGenericParam(BaseModel):
|
||||
)
|
||||
tool_call_id: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
phase: Optional[Literal["commentary", "final_answer"]] = None
|
||||
reasoning_content: Optional[str] = None
|
||||
tool_calls: Optional[List[ToolCall]] = Field(default=None, examples=[None])
|
||||
tools: Optional[List[Tool]] = Field(default=None, examples=[None])
|
||||
@@ -1581,6 +1585,9 @@ class ResponseTool(BaseModel):
|
||||
strict: bool = False
|
||||
# Inner schemas for ``namespace`` tools.
|
||||
tools: Optional[List[Dict[str, Any]]] = None
|
||||
# Input format of a ``custom`` tool: {"type": "text"} or
|
||||
# {"type": "grammar", "syntax": ..., "definition": ...}.
|
||||
format: Optional[Dict[str, Any]] = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_function_tool(self) -> ResponseTool:
|
||||
@@ -1589,7 +1596,17 @@ class ResponseTool(BaseModel):
|
||||
return self
|
||||
|
||||
|
||||
class ResponseInputMessageParam(EasyInputMessageParam, total=False):
|
||||
phase: Optional[Literal["commentary", "final_answer"]]
|
||||
|
||||
|
||||
class ResponseOutputMessage(OpenAIResponseOutputMessage):
|
||||
phase: Optional[Literal["commentary", "final_answer"]] = None
|
||||
|
||||
|
||||
ResponseInputOutputItem: TypeAlias = Union[
|
||||
ResponseInputMessageParam,
|
||||
ResponseOutputMessage,
|
||||
ResponseInputItemParam,
|
||||
"ResponseReasoningItem",
|
||||
ResponseFunctionToolCall,
|
||||
@@ -1782,20 +1799,24 @@ class ResponsesRequest(BaseModel):
|
||||
def is_include_output_logprobs(self) -> bool:
|
||||
return bool(self.include and "message.output_text.logprobs" in self.include)
|
||||
|
||||
def is_include_encrypted_reasoning(self) -> bool:
|
||||
return bool(self.include and "reasoning.encrypted_content" in self.include)
|
||||
|
||||
def has_json_schema_constraint(self) -> bool:
|
||||
return self._json_schema_from_text_format(self.text) is not None
|
||||
|
||||
def effective_tool_choice(self) -> Union[str, Dict[str, Any]]:
|
||||
"""``tool_choice`` reduced to what the server can actually honor: of the
|
||||
object forms only a named ``function`` survives, the rest (web_search,
|
||||
mcp, ...) can't be forced through the tool-call parser."""
|
||||
object forms only a named ``function`` / ``custom`` tool survives, the
|
||||
rest (web_search, mcp, ...) can't be forced through the tool-call
|
||||
parser."""
|
||||
tool_choice = self.tool_choice
|
||||
if not isinstance(tool_choice, dict):
|
||||
return tool_choice
|
||||
name = tool_choice.get("name") or (tool_choice.get("function") or {}).get(
|
||||
"name"
|
||||
)
|
||||
if tool_choice.get("type") == "function" and name:
|
||||
if tool_choice.get("type") in ("function", "custom") and name:
|
||||
return {"type": "function", "name": name}
|
||||
return "auto"
|
||||
|
||||
@@ -1897,7 +1918,12 @@ class ResponsesResponse(BaseModel):
|
||||
model: str
|
||||
|
||||
output: List[
|
||||
Union[ResponseOutputItem, ResponseReasoningItem, ResponseFunctionToolCall]
|
||||
Union[
|
||||
ResponseOutputMessage,
|
||||
ResponseOutputItem,
|
||||
ResponseReasoningItem,
|
||||
ResponseFunctionToolCall,
|
||||
]
|
||||
] = Field(default_factory=list)
|
||||
status: Literal[
|
||||
"queued", "in_progress", "completed", "incomplete", "failed", "cancelled"
|
||||
@@ -1957,7 +1983,12 @@ class ResponsesResponse(BaseModel):
|
||||
model_name: str,
|
||||
created_time: int,
|
||||
output: List[
|
||||
Union[ResponseOutputItem, ResponseReasoningItem, ResponseFunctionToolCall]
|
||||
Union[
|
||||
ResponseOutputMessage,
|
||||
ResponseOutputItem,
|
||||
ResponseReasoningItem,
|
||||
ResponseFunctionToolCall,
|
||||
]
|
||||
],
|
||||
status: str,
|
||||
usage: Optional[UsageInfo],
|
||||
@@ -1983,7 +2014,7 @@ class ResponsesResponse(BaseModel):
|
||||
try:
|
||||
if isinstance(it, ResponseOutputText):
|
||||
continue
|
||||
elif isinstance(it, ResponseOutputMessage):
|
||||
elif isinstance(it, OpenAIResponseOutputMessage):
|
||||
if not it.content:
|
||||
continue
|
||||
for c in it.content:
|
||||
@@ -2083,7 +2114,11 @@ class ResponseReasoningTextContent(BaseModel):
|
||||
|
||||
|
||||
ResponseInputOutputItem: TypeAlias = Union[
|
||||
ResponseInputItemParam, "ResponseReasoningItem", ResponseFunctionToolCall
|
||||
ResponseInputMessageParam,
|
||||
ResponseOutputMessage,
|
||||
ResponseInputItemParam,
|
||||
"ResponseReasoningItem",
|
||||
ResponseFunctionToolCall,
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Translations between Responses-API wire shapes and SGLang's chat internals.
|
||||
|
||||
Two things the Responses API expresses that the chat-completions path has no
|
||||
representation for:
|
||||
|
||||
* ``custom`` tools, whose payload is freeform text rather than JSON-object
|
||||
arguments. Each is surfaced to the model as a function tool with a single
|
||||
string property, and the resulting call is translated back into a
|
||||
``custom_tool_call``.
|
||||
* ``reasoning.encrypted_content``, the opaque blob a ``store=false`` client
|
||||
replays to hand a reasoning trace back to the server.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import zlib
|
||||
from typing import Any, Dict, Optional, Set, Tuple
|
||||
|
||||
CUSTOM_TOOL_INPUT_KEY = "input"
|
||||
|
||||
_SIMPLE_ESCAPES = {
|
||||
'"': '"',
|
||||
"\\": "\\",
|
||||
"/": "/",
|
||||
"b": "\b",
|
||||
"f": "\f",
|
||||
"n": "\n",
|
||||
"r": "\r",
|
||||
"t": "\t",
|
||||
}
|
||||
|
||||
|
||||
def custom_tool_parameters() -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
CUSTOM_TOOL_INPUT_KEY: {
|
||||
"type": "string",
|
||||
"description": "The freeform text payload for this tool.",
|
||||
}
|
||||
},
|
||||
"required": [CUSTOM_TOOL_INPUT_KEY],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
|
||||
def custom_tool_description(
|
||||
description: Optional[str], tool_format: Optional[Dict[str, Any]]
|
||||
) -> Optional[str]:
|
||||
"""Fold a custom tool's description and its input format into one blob.
|
||||
|
||||
A ``grammar`` format is described to the model rather than enforced by the
|
||||
grammar backend: the payload is emitted inside the model's own tool-call
|
||||
envelope, so constrained decoding cannot be scoped to it.
|
||||
"""
|
||||
parts = []
|
||||
if description:
|
||||
parts.append(description)
|
||||
if isinstance(tool_format, dict) and tool_format.get("type") == "grammar":
|
||||
syntax = tool_format.get("syntax") or "lark"
|
||||
definition = tool_format.get("definition") or ""
|
||||
parts.append(
|
||||
f"The payload must conform to this {syntax} grammar:\n{definition}"
|
||||
)
|
||||
return "\n\n".join(parts) or None
|
||||
|
||||
|
||||
def custom_tool_names(tools: Any) -> Set[str]:
|
||||
return {tool.name for tool in tools or [] if tool.type == "custom" and tool.name}
|
||||
|
||||
|
||||
def encode_custom_tool_input(payload: str) -> str:
|
||||
return json.dumps({CUSTOM_TOOL_INPUT_KEY: payload}, ensure_ascii=False)
|
||||
|
||||
|
||||
def decode_custom_tool_input(arguments: str) -> str:
|
||||
"""Recover the payload from a completed shim call's arguments.
|
||||
|
||||
Falls back to the raw string so a model that ignored the wrapper and
|
||||
emitted bare text still produces a usable call.
|
||||
"""
|
||||
try:
|
||||
parsed = json.loads(arguments)
|
||||
except ValueError:
|
||||
return arguments
|
||||
if isinstance(parsed, dict):
|
||||
value = parsed.get(CUSTOM_TOOL_INPUT_KEY)
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
if len(parsed) == 1:
|
||||
only = next(iter(parsed.values()))
|
||||
if isinstance(only, str):
|
||||
return only
|
||||
return arguments
|
||||
|
||||
|
||||
def _payload_start(buffer: str) -> Optional[int]:
|
||||
key = f'"{CUSTOM_TOOL_INPUT_KEY}"'
|
||||
key_at = buffer.find(key)
|
||||
if key_at < 0:
|
||||
return None
|
||||
colon = buffer.find(":", key_at + len(key))
|
||||
if colon < 0:
|
||||
return None
|
||||
quote = buffer.find('"', colon + 1)
|
||||
return quote + 1 if quote >= 0 else None
|
||||
|
||||
|
||||
def _unicode_escape(buffer: str, i: int) -> Optional[Tuple[str, int]]:
|
||||
if i + 6 > len(buffer):
|
||||
return None
|
||||
try:
|
||||
code = int(buffer[i + 2 : i + 6], 16)
|
||||
except ValueError:
|
||||
return None
|
||||
if not 0xD800 <= code <= 0xDBFF:
|
||||
return chr(code), i + 6
|
||||
if i + 12 > len(buffer) or buffer[i + 6 : i + 8] != "\\u":
|
||||
return None
|
||||
try:
|
||||
low = int(buffer[i + 8 : i + 12], 16)
|
||||
except ValueError:
|
||||
return None
|
||||
if not 0xDC00 <= low <= 0xDFFF:
|
||||
return None
|
||||
return chr(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00)), i + 12
|
||||
|
||||
|
||||
def decode_custom_tool_input_prefix(buffer: str) -> str:
|
||||
"""Longest fully decodable prefix of the payload in a partial argument buffer.
|
||||
|
||||
Streaming tool-call parsers hand out argument JSON in fragments, so the
|
||||
payload is un-escaped incrementally to keep ``custom_tool_call_input``
|
||||
deltas in step with the value the item finally reports.
|
||||
"""
|
||||
start = _payload_start(buffer)
|
||||
if start is None:
|
||||
return ""
|
||||
out = []
|
||||
i, n = start, len(buffer)
|
||||
while i < n:
|
||||
ch = buffer[i]
|
||||
if ch == '"':
|
||||
break
|
||||
if ch != "\\":
|
||||
out.append(ch)
|
||||
i += 1
|
||||
continue
|
||||
if i + 1 >= n:
|
||||
break
|
||||
simple = _SIMPLE_ESCAPES.get(buffer[i + 1])
|
||||
if simple is not None:
|
||||
out.append(simple)
|
||||
i += 2
|
||||
continue
|
||||
if buffer[i + 1] == "u":
|
||||
decoded = _unicode_escape(buffer, i)
|
||||
if decoded is None:
|
||||
break
|
||||
out.append(decoded[0])
|
||||
i = decoded[1]
|
||||
continue
|
||||
break
|
||||
return "".join(out)
|
||||
|
||||
|
||||
DEVELOPER_BLOCK_LABEL = "Developer instructions:"
|
||||
|
||||
|
||||
def label_developer_content(content: Any) -> Any:
|
||||
"""Prefix a ``developer`` message's text with its tier label.
|
||||
|
||||
Chat templates recognize system/user/assistant/tool, so a developer message
|
||||
collapses to ``system``. Labelling the block keeps the tier legible instead
|
||||
of flattening it into the surrounding instructions, where a model can read
|
||||
it as one more conversational turn and let a later user message win.
|
||||
"""
|
||||
if isinstance(content, str):
|
||||
return f"{DEVELOPER_BLOCK_LABEL}\n{content}"
|
||||
if isinstance(content, list):
|
||||
for index, part in enumerate(content):
|
||||
if isinstance(part, dict) and isinstance(part.get("text"), str):
|
||||
labelled = dict(part)
|
||||
labelled["text"] = f"{DEVELOPER_BLOCK_LABEL}\n{part['text']}"
|
||||
return [*content[:index], labelled, *content[index + 1 :]]
|
||||
return [{"type": "input_text", "text": DEVELOPER_BLOCK_LABEL}, *content]
|
||||
return content
|
||||
|
||||
|
||||
_REASONING_STATE_PREFIX = "sglang-reasoning-v1."
|
||||
|
||||
|
||||
def encode_reasoning_state(text: str) -> str:
|
||||
"""Pack a reasoning trace into the blob ``reasoning.encrypted_content`` carries.
|
||||
|
||||
A ``store=false`` client replays reasoning items verbatim, so the trace has
|
||||
to survive the round trip without server-side state. SGLang holds no key,
|
||||
so this is an encoded -- not cryptographically protected -- payload: treat
|
||||
it as opaque to clients, not as a confidentiality boundary.
|
||||
"""
|
||||
packed = base64.urlsafe_b64encode(zlib.compress(text.encode("utf-8")))
|
||||
return _REASONING_STATE_PREFIX + packed.decode("ascii")
|
||||
|
||||
|
||||
def decode_reasoning_state(blob: Any) -> Optional[str]:
|
||||
if not isinstance(blob, str) or not blob.startswith(_REASONING_STATE_PREFIX):
|
||||
return None
|
||||
try:
|
||||
raw = base64.urlsafe_b64decode(blob[len(_REASONING_STATE_PREFIX) :])
|
||||
return zlib.decompress(raw).decode("utf-8")
|
||||
except (ValueError, zlib.error, UnicodeDecodeError):
|
||||
return None
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user