mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-10-27 17:03:49 +08:00
feat(llm): 支持 OpenAI Responses API 格式的 LLM 配置
依赖升级到 norma v0.3.1(新增 FormatOpenAIResponses + reasoning_content 回传 修复)。每个 LLM profile 现在可选第三种格式 openai-responses,打 POST /v1/responses,与 Chat Completions / Anthropic 并列。 - agent.Config:ConfigFrom / FromEnv / Provider() 支持 openai-responses (BaseURL 归一化去 /responses 尾巴,默认 model gpt-5) - server:saveLLMConfig 放开二元 format 归一;模型列表端点复用 OpenAI /models(Responses 模型列表仍在 /v1/models) - db: llm_profiles 的 format CHECK 约束加入 openai-responses,并幂等迁移 (DROP + ADD CONSTRAINT)补旧库 - web: 格式下拉新增「OpenAI (Responses API)」,类型与回显同步 流式/非流式两条路都走新 adapter;输出、token 统计、操作统计、LLM 录制 四条链路复用中性层,无需改动。
This commit is contained in:
1 parent
587ba422b4
commit
12541a6dec
9 files changed
+71
-16
No files matched your search
+18
-2
@@ -132,6 +132,12 @@ func FromEnv() (Config, bool) {
|
||||
if c.Model == "" {
|
||||
c.Model = "gpt-4o"
|
||||
}
|
||||
case "openai-responses":
|
||||
c.Format = llm.FormatOpenAIResponses
|
||||
c.APIKey = oaiKey
|
||||
if c.Model == "" {
|
||||
c.Model = "gpt-5"
|
||||
}
|
||||
default:
|
||||
c.Format = llm.FormatAnthropic
|
||||
c.APIKey = anthKey
|
||||
@@ -165,6 +171,13 @@ func ConfigFrom(provider, model, baseURL, apiKey, proxy string) Config {
|
||||
if c.Model == "" {
|
||||
c.Model = "gpt-4o"
|
||||
}
|
||||
case "openai-responses":
|
||||
c.Format = llm.FormatOpenAIResponses
|
||||
// provider appends "/responses"; tolerate a full endpoint URL.
|
||||
c.BaseURL = strings.TrimRight(strings.TrimSuffix(c.BaseURL, "/responses"), "/")
|
||||
if c.Model == "" {
|
||||
c.Model = "gpt-5"
|
||||
}
|
||||
default:
|
||||
c.Format = llm.FormatAnthropic
|
||||
// provider appends "/v1/messages".
|
||||
@@ -186,10 +199,13 @@ func isFalsy(s string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Provider returns the short provider name ("anthropic"/"openai").
|
||||
// Provider returns the short provider name ("anthropic"/"openai"/"openai-responses").
|
||||
func (c Config) Provider() string {
|
||||
if c.Format == llm.FormatOpenAI {
|
||||
switch c.Format {
|
||||
case llm.FormatOpenAI:
|
||||
return "openai"
|
||||
case llm.FormatOpenAIResponses:
|
||||
return "openai-responses"
|
||||
}
|
||||
return "anthropic"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Autumn-27/norma/llm"
|
||||
)
|
||||
|
||||
// TestConfigFromOpenAIResponses locks the openai-responses format wiring:
|
||||
// ConfigFrom resolves the Responses format, strips a full /responses endpoint
|
||||
// back to the API base, and Provider() round-trips the short name. NewProvider
|
||||
// must build a working provider for it.
|
||||
func TestConfigFromOpenAIResponses(t *testing.T) {
|
||||
c := ConfigFrom("openai-responses", "gpt-5", "https://gw.example/v1/responses", "sk-x", "")
|
||||
if c.Format != llm.FormatOpenAIResponses {
|
||||
t.Fatalf("format=%v, want FormatOpenAIResponses", c.Format)
|
||||
}
|
||||
if c.BaseURL != "https://gw.example/v1" {
|
||||
t.Fatalf("base_url=%q, want the /responses suffix stripped", c.BaseURL)
|
||||
}
|
||||
if c.Provider() != "openai-responses" {
|
||||
t.Fatalf("Provider()=%q", c.Provider())
|
||||
}
|
||||
if !c.Stream { // default streaming preserved
|
||||
t.Fatal("Stream should default true")
|
||||
}
|
||||
if _, err := c.NewProvider(); err != nil {
|
||||
t.Fatalf("NewProvider: %v", err)
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -284,7 +284,7 @@ CREATE TABLE IF NOT EXISTS settings (
|
||||
CREATE TABLE IF NOT EXISTS llm_profiles (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
format TEXT NOT NULL CHECK (format IN ('openai','anthropic')),
|
||||
format TEXT NOT NULL CHECK (format IN ('openai','anthropic','openai-responses')),
|
||||
base_url TEXT,
|
||||
proxy TEXT,
|
||||
model TEXT NOT NULL,
|
||||
@@ -317,6 +317,11 @@ ALTER TABLE llm_profiles ADD COLUMN IF NOT EXISTS priority INTEGER NOT NULL
|
||||
ALTER TABLE llm_profiles ADD COLUMN IF NOT EXISTS pool_exclude BOOLEAN NOT NULL DEFAULT false;
|
||||
-- 流式开关;补旧库。默认 true = 保持既有的流式行为,旧配置无感升级。
|
||||
ALTER TABLE llm_profiles ADD COLUMN IF NOT EXISTS streaming BOOLEAN NOT NULL DEFAULT true;
|
||||
-- 放开 format 约束以容纳 openai-responses(OpenAI Responses API);补旧库。
|
||||
-- 每次启动执行,幂等:先删旧 CHECK 再建含三值的新 CHECK。
|
||||
ALTER TABLE llm_profiles DROP CONSTRAINT IF EXISTS llm_profiles_format_check;
|
||||
ALTER TABLE llm_profiles ADD CONSTRAINT llm_profiles_format_check
|
||||
CHECK (format IN ('openai','anthropic','openai-responses'));
|
||||
|
||||
-- 思考开关字段 thinking_type,从旧的单一 reasoning_effort 语义一次性拆分而来。
|
||||
-- schema.sql 每次启动都执行,故迁移必须只跑一次:仅当该列尚不存在时才回填,
|
||||
|
||||
@@ -3,7 +3,7 @@ module github.com/Autumn-27/artex
|
||||
go 1.26.3
|
||||
|
||||
require (
|
||||
github.com/Autumn-27/norma v0.3.0
|
||||
github.com/Autumn-27/norma v0.3.1
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/jackc/pgx/v5 v5.10.0
|
||||
github.com/lqqyt2423/go-mitmproxy v1.9.2
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
github.com/Autumn-27/norma v0.3.0 h1:nbaoeLj65ElFwWdy44U0QCXPe0idaw8tdyyuU/Sdbuw=
|
||||
github.com/Autumn-27/norma v0.3.0/go.mod h1:BudWrf7iHl96FgHqFr/HwN3mKYmGjJDt+xL2ZHXfFZw=
|
||||
github.com/Autumn-27/norma v0.3.1 h1:cSRojFJjivm4l3ck/KJXym7VyCnM1GLY7iq1pRYk4kk=
|
||||
github.com/Autumn-27/norma v0.3.1/go.mod h1:BudWrf7iHl96FgHqFr/HwN3mKYmGjJDt+xL2ZHXfFZw=
|
||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
|
||||
+2
-3
@@ -317,10 +317,9 @@ func (s *Server) loadLLMConfig() (agent.Config, bool) {
|
||||
|
||||
// saveLLMConfig persists the LLM config as the active "default" profile in PG.
|
||||
func (s *Server) saveLLMConfig(cfg agent.Config) error {
|
||||
// cfg.Provider() already returns one of the three valid format strings
|
||||
// (anthropic / openai / openai-responses), matching the DB CHECK constraint.
|
||||
format := cfg.Provider()
|
||||
if format != "anthropic" {
|
||||
format = "openai"
|
||||
}
|
||||
var id int64
|
||||
// agent.Config carries no failover fields, so carry the stored ones forward —
|
||||
// otherwise this legacy endpoint would silently reset the profile's priority,
|
||||
|
||||
@@ -1926,11 +1926,13 @@ func (s *Server) pgListModels(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var candidates []candidate
|
||||
switch provider {
|
||||
case "openai":
|
||||
case "openai", "openai-responses":
|
||||
// Responses API shares the OpenAI model list at /v1/models; tolerate a full
|
||||
// endpoint URL of either format.
|
||||
if baseURL == "" {
|
||||
baseURL = "https://api.openai.com/v1"
|
||||
}
|
||||
b := strings.TrimRight(strings.TrimSuffix(baseURL, "/chat/completions"), "/")
|
||||
b := strings.TrimRight(strings.TrimSuffix(strings.TrimSuffix(baseURL, "/chat/completions"), "/responses"), "/")
|
||||
candidates = append(candidates, candidate{b + "/models", bearerHdr()})
|
||||
default: // anthropic
|
||||
if baseURL == "" {
|
||||
|
||||
@@ -296,7 +296,7 @@ function ProfileSheet({
|
||||
}) {
|
||||
const isNew = !profile;
|
||||
const [name, setName] = React.useState("");
|
||||
const [format, setFormat] = React.useState<"anthropic" | "openai">("anthropic");
|
||||
const [format, setFormat] = React.useState<"anthropic" | "openai" | "openai-responses">("anthropic");
|
||||
const [model, setModel] = React.useState("");
|
||||
const [baseUrl, setBaseUrl] = React.useState("");
|
||||
const [proxy, setProxy] = React.useState("");
|
||||
@@ -321,7 +321,9 @@ function ProfileSheet({
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
setName(profile?.name ?? "");
|
||||
setFormat(profile?.format === "openai" ? "openai" : "anthropic");
|
||||
setFormat(
|
||||
profile?.format === "openai" || profile?.format === "openai-responses" ? profile.format : "anthropic",
|
||||
);
|
||||
setModel(profile?.model ?? "");
|
||||
setBaseUrl(profile?.base_url ?? "");
|
||||
setProxy(profile?.proxy ?? "");
|
||||
@@ -448,13 +450,14 @@ function ProfileSheet({
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>格式</Label>
|
||||
<Select value={format} onValueChange={(v) => setFormat(v as "anthropic" | "openai")}>
|
||||
<Select value={format} onValueChange={(v) => setFormat(v as "anthropic" | "openai" | "openai-responses")}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择格式" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="anthropic">Anthropic</SelectItem>
|
||||
<SelectItem value="openai">OpenAI</SelectItem>
|
||||
<SelectItem value="openai">OpenAI (Chat Completions)</SelectItem>
|
||||
<SelectItem value="openai-responses">OpenAI (Responses API)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -782,7 +782,7 @@ export interface Settings {
|
||||
export interface LLMProfile {
|
||||
id: string;
|
||||
name: string;
|
||||
format: "openai" | "anthropic";
|
||||
format: "openai" | "anthropic" | "openai-responses";
|
||||
base_url?: string;
|
||||
proxy?: string;
|
||||
model: string;
|
||||
|
||||
Reference in new issue
Block a user