-
Notifications
You must be signed in to change notification settings - Fork 45
Expand file tree
/
Copy pathopenai_format.py
More file actions
120 lines (95 loc) · 3.72 KB
/
Copy pathopenai_format.py
File metadata and controls
120 lines (95 loc) · 3.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
"""Translate between OpenAI's chat-completions shapes and our DeepSeek client.
DeepSeek's protocol has no system/role channel — just a single `prompt` string.
So we flatten the OpenAI `messages` array into one prompt, and wrap DeepSeek's
text output back into OpenAI response/stream objects.
"""
from __future__ import annotations
import json
import time
import uuid
from typing import Iterable, List
from .schemas import ChatMessage
_ROLE_LABELS = {"system": "System", "user": "User", "assistant": "Assistant"}
def _text_of(content) -> str:
"""Extract plain text from a message's content (string or list-of-parts)."""
if content is None:
return ""
if isinstance(content, str):
return content
parts = []
for p in content:
if isinstance(p, dict) and p.get("type") == "text":
parts.append(p.get("text", ""))
return "\n".join(parts)
def messages_to_prompt(messages: List[ChatMessage]) -> str:
"""Flatten a chat history into a single prompt DeepSeek can answer.
A lone user message is sent verbatim. Multi-turn / system-prompted
conversations are serialised with role labels and a trailing 'Assistant:'
cue so the model continues in the right voice.
"""
if len(messages) == 1 and messages[0].role == "user":
return _text_of(messages[0].content)
lines = []
for m in messages:
label = _ROLE_LABELS.get(m.role, m.role.capitalize())
lines.append(f"{label}: {_text_of(m.content)}")
lines.append("Assistant:")
return "\n\n".join(lines)
def _now() -> int:
return int(time.time())
def _id() -> str:
return "chatcmpl-" + uuid.uuid4().hex
def _est_tokens(text: str) -> int:
"""Rough token estimate (~4 chars/token) — DeepSeek's web API gives us no count."""
return max(1, len(text) // 4)
def completion_response(model: str, content: str, prompt: str,
conversation_id: str = None) -> dict:
"""A full (non-streaming) OpenAI chat.completion object.
`conversation_id` is an extra top-level field (outside OpenAI's schema) you
send back to resume the conversation.
"""
pt, ct = _est_tokens(prompt), _est_tokens(content)
return {
"id": _id(),
"object": "chat.completion",
"created": _now(),
"model": model,
"conversation_id": conversation_id,
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": content},
"finish_reason": "stop",
}
],
"usage": {
"prompt_tokens": pt,
"completion_tokens": ct,
"total_tokens": pt + ct,
},
}
def stream_chunks(model: str, stream: Iterable[str]) -> Iterable[str]:
"""Yield OpenAI SSE lines (`data: {...}\\n\\n`) for a streamed completion.
`stream` is the client's stream object; after it's consumed we read its
`.conversation_id` and attach it to the final chunk.
"""
cid, created = _id(), _now()
def frame(delta: dict, finish=None, extra: dict = None) -> str:
obj = {
"id": cid,
"object": "chat.completion.chunk",
"created": created,
"model": model,
"choices": [{"index": 0, "delta": delta, "finish_reason": finish}],
}
if extra:
obj.update(extra)
return f"data: {json.dumps(obj, ensure_ascii=False)}\n\n"
# First frame announces the assistant role.
yield frame({"role": "assistant", "content": ""})
for d in stream:
if d:
yield frame({"content": d})
conversation_id = getattr(stream, "conversation_id", None)
yield frame({}, finish="stop", extra={"conversation_id": conversation_id})
yield "data: [DONE]\n\n"