mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-21 23:41:39 +08:00
feat(openai_gateway): 纯文本可单独使用 OPENAI_TEXT_* 凭据
chat_completion_text 经 resolve_text_channel_credentials;与配料/多模 OPENAI_* 可不同 Key 或不同 BASE_URL;单测与 .env.example 更新。 Made-with: Cursor
This commit is contained in:
parent
f3975a0fd3
commit
be05dd3577
13
.env.example
13
.env.example
@ -21,20 +21,23 @@ DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
|
||||
CORS_ALLOWED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
|
||||
CSRF_TRUSTED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
|
||||
|
||||
# --- LLM ① 自建/兼容网关(与 pipeline.openai_gateway 共用;商详多模、配料/视觉等走 OPENAI_*。若 ② 选方案 A,纯文本也读同一套 Key、BASE_URL,仅「文本模型名」可经 OPENAI_TEXT_MODEL 与 OPENAI_VISION_MODEL 不同;与 ① 完全独立网关/Key 时需改 ②-B~②-D)---
|
||||
# --- LLM ① 自建/兼容网关(与 pipeline.openai_gateway 共用;商详多模、配料/视觉走下面 OPENAI_* + OPENAI_VISION_MODEL)---
|
||||
# OPENAI_API_KEY=sk-your-key-here
|
||||
# OPENAI_BASE_URL=https://llm.example.com/v1
|
||||
# OPENAI_VISION_MODEL=Qwen/Qwen3-Omni-30B-A3B
|
||||
# 若 OPENAI_BASE_URL=https://llm.rekeymed.com/v1,可用同一 Key 调 GET /v1/models 查看当前账号可见的 model id;以下为某次查询示例(以实际返回为准):
|
||||
# 若 ② 选方案 A:纯文本默认也走 ① 的 Key/基址,但可只用下面两枚变量「只拆纯文本」:任一项不设则回退 ①
|
||||
# OPENAI_TEXT_API_KEY=(与配料/视觉同网关不同 Key、或分账号)
|
||||
# OPENAI_TEXT_BASE_URL=(可仅换路径/集群,仍与 ① 同域名)
|
||||
# 若 OPENAI_BASE_URL=https://llm.rekeymed.com/v1,可用 Key 调 GET /v1/models 看 id;以下为某次查询示例(以实际返回为准):
|
||||
# Qwen/Qwen3-Omni-30B-A3B
|
||||
# openai/gpt-oss-120b
|
||||
# zai-org/GLM-4.5-Air
|
||||
# 仅当 ② 选「方案 A」且要在自建上单独换「只文本补全」模型时填写;不填时回退为视觉模型名或网关默认
|
||||
# 纯文本专用模型名(不填时回退 OPENAI_VISION_MODEL / LLM_MODEL 等;与多模可不同)
|
||||
# OPENAI_TEXT_MODEL=
|
||||
# 别名:LLM_API_KEY、LLM_BASE_URL、LLM_MODEL
|
||||
# 别名:LLM_API_KEY、LLM_BASE_URL、LLM_MODEL、LLM_TEXT_API_KEY、LLM_TEXT_BASE_URL
|
||||
|
||||
# --- LLM ② 纯文本(报告/策略/关键词等 call_llm;与 ②-B~②-D 多选一,只让一条 MA_LLM_TEXT_PROVIDER 生效,换方案时先改/注释本行再启别节)---
|
||||
# 方案 A(默认)= 经 openai_gateway 调你「当前 .env 里」的 OPENAI_*(不绑死 ① 里某条示例,可换任意兼容网关);要换 B=官站、C=Kimi、D=DeepSeek 时,把下面这行改成 openai_official|kimi|deepseek 等,并去对应小节填凭据
|
||||
# 方案 A(默认)= 经 openai_gateway:纯文本读 OPENAI_TEXT_*(若有)否则 OPENAI_*;要换 B=官站、C=Kimi、D=DeepSeek 时改下行为 openai_official|kimi|deepseek 等
|
||||
MA_LLM_TEXT_PROVIDER=crawler_openai_compatible
|
||||
|
||||
# --- ②-B 纯文本用 OpenAI 官站(仅当 MA=openai_official / openai_chatgpt / chatgpt;与 ②-A、②-C 互斥;凭据与 ① 可不同)---
|
||||
|
||||
@ -9,6 +9,7 @@ from .chat_content import normalize_message_content
|
||||
from .credentials import (
|
||||
_resolve_credentials,
|
||||
resolve_credentials,
|
||||
resolve_text_channel_credentials,
|
||||
resolve_text_model_name,
|
||||
)
|
||||
from .ingredients_op import (
|
||||
@ -35,6 +36,7 @@ __all__ = [
|
||||
"normalize_message_content",
|
||||
"parse_joined_image_urls",
|
||||
"resolve_credentials",
|
||||
"resolve_text_channel_credentials",
|
||||
"resolve_text_model_name",
|
||||
"sanitize_vision_ingredients_output",
|
||||
"strip_outer_markdown_fence",
|
||||
|
||||
@ -38,6 +38,37 @@ def _resolve_credentials(
|
||||
return key, base, m
|
||||
|
||||
|
||||
def resolve_text_channel_credentials(
|
||||
api_key: str | None = None,
|
||||
base_url: str | None = None,
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
仅用于 ``text_chat.chat_completion_text`` 的 Key 与基址。
|
||||
|
||||
可与商详多模/配料的 ``OPENAI_API_KEY``、``OPENAI_BASE_URL`` **分开**(例如同网关两枚 Key、或同 Key 不同路径),
|
||||
任一侧未设 ``OPENAI_TEXT_*`` 时回退到 ``OPENAI_*`` / ``LLM_*``。
|
||||
|
||||
别名:``LLM_TEXT_API_KEY``、``LLM_TEXT_BASE_URL``(与 ``OPENAI_TEXT_MODEL`` 等命名一致)。
|
||||
"""
|
||||
key = (api_key or "").strip() or (
|
||||
(os.environ.get("OPENAI_TEXT_API_KEY") or os.environ.get("LLM_TEXT_API_KEY") or "").strip()
|
||||
)
|
||||
if not key:
|
||||
key = (os.environ.get("OPENAI_API_KEY") or os.environ.get("LLM_API_KEY") or "").strip()
|
||||
base = (base_url or "").strip().rstrip("/")
|
||||
if not base:
|
||||
base = (os.environ.get("OPENAI_TEXT_BASE_URL") or os.environ.get("LLM_TEXT_BASE_URL") or "").strip().rstrip(
|
||||
"/"
|
||||
)
|
||||
if not base:
|
||||
base = (os.environ.get("OPENAI_BASE_URL") or os.environ.get("LLM_BASE_URL") or "").strip().rstrip("/")
|
||||
if not key:
|
||||
raise ValueError("纯文本需设置 OPENAI_TEXT_API_KEY 或 OPENAI_API_KEY(或 LLM_* 别名)")
|
||||
if not base:
|
||||
raise ValueError("纯文本需设置 OPENAI_TEXT_BASE_URL 或 OPENAI_BASE_URL(或 LLM_* 别名)")
|
||||
return key, base
|
||||
|
||||
|
||||
def resolve_text_model_name(model: str | None = None) -> str:
|
||||
m = (model or "").strip()
|
||||
if m:
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
"""
|
||||
OpenAI 兼容 `chat/completions` 纯文本(system + user),与多模态配料识别共用 `OPENAI_*` / `LLM_*` 环境配置。
|
||||
OpenAI 兼容 `chat/completions` 纯文本(system + user)。
|
||||
|
||||
凭据与基址可经 ``OPENAI_TEXT_API_KEY`` / ``OPENAI_TEXT_BASE_URL`` 与多模/配料的 ``OPENAI_API_KEY`` / ``OPENAI_BASE_URL`` **分开**(可只覆写其中一项,另一项回退到 ``OPENAI_*``)。详见 ``credentials.resolve_text_channel_credentials``。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@ -9,7 +11,7 @@ from typing import Any
|
||||
import requests
|
||||
|
||||
from .chat_content import normalize_message_content
|
||||
from .credentials import _resolve_credentials, resolve_text_model_name
|
||||
from .credentials import resolve_text_channel_credentials, resolve_text_model_name
|
||||
from .estimate import estimate_chat_input_tokens
|
||||
from .timeouts import chat_completion_read_timeout as _chat_completion_timeout
|
||||
|
||||
@ -41,7 +43,7 @@ def chat_completion_text(
|
||||
) -> str:
|
||||
if timeout is None:
|
||||
timeout = _chat_completion_timeout()
|
||||
k, b, _ = _resolve_credentials(api_key, base_url, None)
|
||||
k, b = resolve_text_channel_credentials(api_key, base_url)
|
||||
m = resolve_text_model_name(model)
|
||||
body: dict[str, Any] = {
|
||||
"model": m,
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
"""OPENAI_TEXT_* 与 OPENAI_* 可分开供纯文本使用。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from pipeline.openai_gateway.credentials import resolve_text_channel_credentials
|
||||
|
||||
|
||||
def test_text_channel_falls_back_to_openai_when_no_text_specific(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "sk-main")
|
||||
monkeypatch.setenv("OPENAI_BASE_URL", "https://gw.example.com/v1")
|
||||
monkeypatch.delenv("OPENAI_TEXT_API_KEY", raising=False)
|
||||
monkeypatch.delenv("OPENAI_TEXT_BASE_URL", raising=False)
|
||||
k, b = resolve_text_channel_credentials()
|
||||
assert k == "sk-main"
|
||||
assert b == "https://gw.example.com/v1"
|
||||
|
||||
|
||||
def test_text_channel_uses_text_key_same_base(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "sk-vision")
|
||||
monkeypatch.setenv("OPENAI_BASE_URL", "https://llm.rekeymed.com/v1")
|
||||
monkeypatch.setenv("OPENAI_TEXT_API_KEY", "sk-text-only")
|
||||
k, b = resolve_text_channel_credentials()
|
||||
assert k == "sk-text-only"
|
||||
assert b == "https://llm.rekeymed.com/v1"
|
||||
|
||||
|
||||
def test_text_channel_uses_text_base_same_key(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "sk-shared")
|
||||
monkeypatch.setenv("OPENAI_BASE_URL", "https://a.com/v1")
|
||||
monkeypatch.setenv("OPENAI_TEXT_BASE_URL", "https://b.com/v1")
|
||||
k, b = resolve_text_channel_credentials()
|
||||
assert k == "sk-shared"
|
||||
assert b == "https://b.com/v1"
|
||||
|
||||
|
||||
def test_explicit_args_win_over_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "sk-env")
|
||||
monkeypatch.setenv("OPENAI_BASE_URL", "https://env.com/v1")
|
||||
k, b = resolve_text_channel_credentials("sk-arg", "https://arg.com/v1")
|
||||
assert k == "sk-arg"
|
||||
assert b == "https://arg.com/v1"
|
||||
Loading…
x
Reference in New Issue
Block a user