hub-gif 9fd32a3f84 refactor(pipeline): OpenAI 网关与配料视觉抽离至 openai_gateway
新增 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
2026-04-27 10:24:50 +08:00

101 lines
3.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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"))