mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-25 17:51:37 +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
|
CORS_ALLOWED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
|
||||||
CSRF_TRUSTED_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_API_KEY=sk-your-key-here
|
||||||
# OPENAI_BASE_URL=https://llm.example.com/v1
|
# OPENAI_BASE_URL=https://llm.example.com/v1
|
||||||
# OPENAI_VISION_MODEL=Qwen/Qwen3-Omni-30B-A3B
|
# 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
|
# Qwen/Qwen3-Omni-30B-A3B
|
||||||
# openai/gpt-oss-120b
|
# openai/gpt-oss-120b
|
||||||
# zai-org/GLM-4.5-Air
|
# zai-org/GLM-4.5-Air
|
||||||
# 仅当 ② 选「方案 A」且要在自建上单独换「只文本补全」模型时填写;不填时回退为视觉模型名或网关默认
|
# 纯文本专用模型名(不填时回退 OPENAI_VISION_MODEL / LLM_MODEL 等;与多模可不同)
|
||||||
# OPENAI_TEXT_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 生效,换方案时先改/注释本行再启别节)---
|
# --- 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
|
MA_LLM_TEXT_PROVIDER=crawler_openai_compatible
|
||||||
|
|
||||||
# --- ②-B 纯文本用 OpenAI 官站(仅当 MA=openai_official / openai_chatgpt / chatgpt;与 ②-A、②-C 互斥;凭据与 ① 可不同)---
|
# --- ②-B 纯文本用 OpenAI 官站(仅当 MA=openai_official / openai_chatgpt / chatgpt;与 ②-A、②-C 互斥;凭据与 ① 可不同)---
|
||||||
|
|||||||
@ -9,6 +9,7 @@ from .chat_content import normalize_message_content
|
|||||||
from .credentials import (
|
from .credentials import (
|
||||||
_resolve_credentials,
|
_resolve_credentials,
|
||||||
resolve_credentials,
|
resolve_credentials,
|
||||||
|
resolve_text_channel_credentials,
|
||||||
resolve_text_model_name,
|
resolve_text_model_name,
|
||||||
)
|
)
|
||||||
from .ingredients_op import (
|
from .ingredients_op import (
|
||||||
@ -35,6 +36,7 @@ __all__ = [
|
|||||||
"normalize_message_content",
|
"normalize_message_content",
|
||||||
"parse_joined_image_urls",
|
"parse_joined_image_urls",
|
||||||
"resolve_credentials",
|
"resolve_credentials",
|
||||||
|
"resolve_text_channel_credentials",
|
||||||
"resolve_text_model_name",
|
"resolve_text_model_name",
|
||||||
"sanitize_vision_ingredients_output",
|
"sanitize_vision_ingredients_output",
|
||||||
"strip_outer_markdown_fence",
|
"strip_outer_markdown_fence",
|
||||||
|
|||||||
@ -38,6 +38,37 @@ def _resolve_credentials(
|
|||||||
return key, base, m
|
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:
|
def resolve_text_model_name(model: str | None = None) -> str:
|
||||||
m = (model or "").strip()
|
m = (model or "").strip()
|
||||||
if m:
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
@ -9,7 +11,7 @@ from typing import Any
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from .chat_content import normalize_message_content
|
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 .estimate import estimate_chat_input_tokens
|
||||||
from .timeouts import chat_completion_read_timeout as _chat_completion_timeout
|
from .timeouts import chat_completion_read_timeout as _chat_completion_timeout
|
||||||
|
|
||||||
@ -41,7 +43,7 @@ def chat_completion_text(
|
|||||||
) -> str:
|
) -> str:
|
||||||
if timeout is None:
|
if timeout is None:
|
||||||
timeout = _chat_completion_timeout()
|
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)
|
m = resolve_text_model_name(model)
|
||||||
body: dict[str, Any] = {
|
body: dict[str, Any] = {
|
||||||
"model": m,
|
"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