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:
Autumn-27 committed 2026-08-25 11:39:24 -04:00
1 parent 587ba422b4
commit 12541a6dec
9 files changed
+71 -16

No files matched your search

+18 -2
View File
@@ -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"
}
+30
View File
@@ -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)
}
}