mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-21 23:41:39 +08:00
新增 pipeline/openai_gateway:credentials、text_chat、ingredients_op、ingredients_defaults 等;AI_crawler 仅加载 .env 与重导出。CrawlerOpenAiCompatible 与京东详情/流水线改走 import pipeline.openai_gateway,不再依赖 sys.path 注入爬虫目录。llm/providers/shared 转重导 gateway。详情脚本补充 backend 入 path。 Made-with: Cursor
101 lines
3.3 KiB
Python
101 lines
3.3 KiB
Python
"""
|
||
OpenAI 兼容 `chat/completions` 纯文本(system + user),与多模态配料识别共用 `OPENAI_*` / `LLM_*` 环境配置。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
from typing import Any
|
||
|
||
import requests
|
||
|
||
from .chat_content import normalize_message_content
|
||
from .credentials import _resolve_credentials, resolve_text_model_name
|
||
from .estimate import estimate_chat_input_tokens
|
||
from .timeouts import chat_completion_read_timeout as _chat_completion_timeout
|
||
|
||
|
||
def strip_outer_markdown_fence(text: str) -> str:
|
||
"""若模型用 ``` / ```markdown 包裹全文,去掉最外层围栏。"""
|
||
t = (text or "").strip()
|
||
if not t.startswith("```"):
|
||
return t
|
||
lines = t.split("\n")
|
||
if lines and lines[0].strip().startswith("```"):
|
||
lines = lines[1:]
|
||
while lines and lines[-1].strip() == "```":
|
||
lines = lines[:-1]
|
||
return "\n".join(lines).strip()
|
||
|
||
|
||
def chat_completion_text(
|
||
*,
|
||
system_prompt: str,
|
||
user_prompt: str,
|
||
api_key: str | None = None,
|
||
base_url: str | None = None,
|
||
model: str | None = None,
|
||
temperature: float = 0.2,
|
||
max_tokens: int = 8192,
|
||
timeout: int | tuple[float, float] | None = None,
|
||
extra_json: dict[str, Any] | None = None,
|
||
) -> str:
|
||
if timeout is None:
|
||
timeout = _chat_completion_timeout()
|
||
k, b, _ = _resolve_credentials(api_key, base_url, None)
|
||
m = resolve_text_model_name(model)
|
||
body: dict[str, Any] = {
|
||
"model": m,
|
||
"messages": [
|
||
{"role": "system", "content": system_prompt},
|
||
{"role": "user", "content": user_prompt},
|
||
],
|
||
"temperature": temperature,
|
||
"max_tokens": max_tokens,
|
||
}
|
||
if extra_json:
|
||
body.update(extra_json)
|
||
ctx_raw = (
|
||
os.environ.get("LLM_CONTEXT_WINDOW")
|
||
or os.environ.get("OPENAI_CONTEXT_WINDOW")
|
||
or "32768"
|
||
).strip()
|
||
try:
|
||
context_window = max(4096, int(ctx_raw))
|
||
except ValueError:
|
||
context_window = 32768
|
||
buf = 256
|
||
input_est = estimate_chat_input_tokens(system_prompt, user_prompt)
|
||
if input_est >= context_window - buf - 256:
|
||
raise ValueError(
|
||
f"提示词过长(估算输入约 {input_est} tokens,上下文上限 {context_window}),"
|
||
"请缩小报告/摘要输入或换更大上下文的模型;也可设置环境变量 LLM_CONTEXT_WINDOW。"
|
||
)
|
||
avail = context_window - input_est - buf
|
||
want = int(body.get("max_tokens") or max_tokens)
|
||
body["max_tokens"] = max(256, min(want, max(avail, 256)))
|
||
r = requests.post(
|
||
f"{b}/chat/completions",
|
||
headers={
|
||
"Authorization": f"Bearer {k}",
|
||
"Content-Type": "application/json",
|
||
},
|
||
json=body,
|
||
timeout=timeout,
|
||
)
|
||
try:
|
||
r.raise_for_status()
|
||
except requests.HTTPError as e:
|
||
snippet = ""
|
||
if e.response is not None:
|
||
snippet = (e.response.text or "")[:1200].replace("\r\n", "\n").replace("\n", " ")
|
||
if snippet:
|
||
raise requests.HTTPError(
|
||
f"{e!s} | body: {snippet}",
|
||
response=e.response,
|
||
request=e.request,
|
||
) from e
|
||
raise
|
||
data = r.json()
|
||
msg = (data.get("choices") or [{}])[0].get("message") or {}
|
||
return normalize_message_content(msg.get("content"))
|