Fix: rm tenant llm call (#17476)

This commit is contained in:
Lynn
2026-07-28 15:54:44 +08:00
committed by GitHub
parent fe38d5f246
commit 675c35a2de
5 changed files with 67 additions and 129 deletions

View File

@@ -45,7 +45,7 @@ import (
"errors"
"fmt"
"net/url"
"regexp"
"strings"
"ragflow/internal/agent/runtime"
@@ -125,27 +125,6 @@ type browserParam struct {
Timeout int `json:"timeout"`
}
// llmIDPattern matches `ModelName@Factory`. The factory part is
// optional; when absent, the caller's tenant lookup will be
// `GetByTenantAndModelName` instead of
// `GetByTenantFactoryAndModelName`.
var llmIDPattern = regexp.MustCompile(`^(.+)@(.+)$`)
// resolveLLMID splits `llm_id` (e.g. "deepseek-v4-pro@DeepSeek") into
// `(modelName, factory)`. When no `@` is present, factory is empty
// and the caller must use a single-key lookup.
//
// Mirrors the contract of `dao.splitModelNameAndFactory` (private);
// re-implemented here to keep the component free of an import
// dependency on a DB-validating private helper.
func resolveLLMID(llmID string) (modelName, factory string) {
m := llmIDPattern.FindStringSubmatch(strings.TrimSpace(llmID))
if m == nil {
return strings.TrimSpace(llmID), ""
}
return strings.TrimSpace(m[1]), strings.TrimSpace(m[2])
}
// Update copies a fresh param map into the receiver. The
// `llm_id`/`model_id` and `prompts`/`prompt` alias pairs collapse
// onto the same field; the first non-empty value wins.
@@ -437,35 +416,24 @@ func (b *BrowserComponent) Outputs() map[string]string {
}
}
// resolveBrowserLLM resolves the Browser's selected model into the model name
// and credentials required by the stagehand runtime. It first tries a
// tenant_model.id lookup, then model@factory parsing via resolveTenantLLM.
// resolveBrowserLLM resolves tenant model credentials exclusively through the
// model_provider series tables (tenant_model → tenant_model_provider →
// tenant_model_instance). It no longer falls back to the legacy tenant_llm path.
//
// Tests override the lookup via `tenantLLMLookupForTest` (a
// package-level function variable) so they don't need a real DB.
// Production code leaves the variable unset.
// Tests override the lookup via `browserLLMLookupForTest` (a package-level
// function variable) so they don't need a real DB. Production code leaves the
// variable unset.
func resolveBrowserLLM(ctx context.Context, db *gorm.DB, tenantID, llmID string) (providerName, modelName, apiKey, baseURL string, err error) {
if tenantLLMLookupForTest != nil {
oldModelName, factory := resolveLLMID(llmID)
apiKey, baseURL, err = tenantLLMLookupForTest(tenantID, oldModelName, factory)
baseURL = browserOpenAICompatibleBaseURL(baseURL, factory)
return factory, oldModelName, apiKey, baseURL, err
if browserLLMLookupForTest != nil {
return browserLLMLookupForTest(ctx, db, tenantID, llmID)
}
providerName, modelName, apiKey, baseURL, err = resolveTenantModelBrowserLLM(ctx, db, tenantID, llmID)
if err == nil {
baseURL = browserOpenAICompatibleBaseURL(baseURL, providerName)
return providerName, modelName, apiKey, baseURL, nil
if err != nil {
return "", "", "", "", fmt.Errorf("tenant model lookup (%q): %w", llmID, err)
}
modelErr := err
oldModelName, factory := resolveLLMID(llmID)
apiKey, baseURL, oldErr := resolveTenantLLM(ctx, db, tenantID, oldModelName, factory)
if oldErr == nil {
baseURL = browserOpenAICompatibleBaseURL(baseURL, factory)
return factory, oldModelName, apiKey, baseURL, nil
}
return "", "", "", "", fmt.Errorf("tenant_model lookup: %v; tenant_llm fallback: %w", modelErr, oldErr)
baseURL = browserOpenAICompatibleBaseURL(baseURL, providerName)
return providerName, modelName, apiKey, baseURL, nil
}
func resolveTenantModelBrowserLLM(ctx context.Context, db *gorm.DB, tenantID, modelID string) (providerName, modelName, apiKey, baseURL string, err error) {
@@ -531,44 +499,10 @@ func browserOpenAICompatibleBaseURL(baseURL, provider string) string {
return browserFactoryDefaultBaseURL[strings.ToLower(provider)]
}
// resolveTenantLLM looks up the legacy tenant_llm config and returns
// (apiKey, baseURL). baseURL may be empty when the tenant's provider doesn't
// configure a custom endpoint.
//
// TODO(v2): this helper can move to `internal/dao` so the LLM
// component (`llm.go`) and other future components can share it.
func resolveTenantLLM(ctx context.Context, db *gorm.DB, tenantID, modelName, factory string) (apiKey, baseURL string, err error) {
llmDAO := dao.NewTenantLLMDAO()
var (
row *entity.TenantLLM
)
if factory != "" {
row, err = llmDAO.GetByTenantFactoryAndModelName(ctx, db, tenantID, factory, modelName)
} else {
// No factory suffix on llm_id; fall back to a single-key
// lookup (errors if the model is registered under multiple
// factories — caller must use the explicit form).
row, err = llmDAO.GetByTenantAndModelName(ctx, db, tenantID, "", modelName)
}
if err != nil {
return "", "", err
}
if row == nil {
return "", "", fmt.Errorf("tenant LLM not found")
}
if row.APIKey != nil {
apiKey = *row.APIKey
}
if row.APIBase != nil {
baseURL = *row.APIBase
}
return apiKey, baseURL, nil
}
// tenantLLMLookupForTest is the test seam for `resolveTenantLLM`.
// browserLLMLookupForTest is the test seam for `resolveBrowserLLM`.
// When non-nil, it's called instead of the real DAO lookup.
// Production leaves this nil; tests set it via `defer ... = nil`.
var tenantLLMLookupForTest func(tenantID, modelName, factory string) (apiKey, baseURL string, err error)
var browserLLMLookupForTest func(ctx context.Context, db *gorm.DB, tenantID, llmID string) (providerName, modelName, apiKey, baseURL string, err error)
func init() {
Register(componentNameBrowser, NewBrowserComponent)

View File

@@ -26,6 +26,8 @@ import (
"ragflow/internal/agent/canvas"
"ragflow/internal/agent/runtime"
"ragflow/internal/entity"
"gorm.io/gorm"
)
// mockStagehandInvoker captures RunExtract requests and returns a
@@ -187,11 +189,11 @@ func TestBrowser_DispatchesToRuntime(t *testing.T) {
mock := &mockStagehandInvoker{rawJSON: `"agent result text"`}
withMockRuntime(t, mock)
prevLookup := tenantLLMLookupForTest
tenantLLMLookupForTest = func(tenantID, modelName, factory string) (string, string, error) {
return "", "", errors.New("fake: tenant LLM not found")
prevLookup := browserLLMLookupForTest
browserLLMLookupForTest = func(ctx context.Context, db *gorm.DB, tenantID, llmID string) (string, string, string, string, error) {
return "", "", "", "", errors.New("fake: tenant LLM not found")
}
t.Cleanup(func() { tenantLLMLookupForTest = prevLookup })
t.Cleanup(func() { browserLLMLookupForTest = prevLookup })
c, _ := NewBrowserComponent(map[string]any{
"llm_id": "deepseek-v4-pro@DeepSeek",
@@ -245,9 +247,9 @@ func TestResolveBrowserLLM_ResolvesTenantModelID(t *testing.T) {
t.Fatalf("create model: %v", err)
}
prevLookup := tenantLLMLookupForTest
tenantLLMLookupForTest = nil
t.Cleanup(func() { tenantLLMLookupForTest = prevLookup })
prevLookup := browserLLMLookupForTest
browserLLMLookupForTest = nil
t.Cleanup(func() { browserLLMLookupForTest = prevLookup })
ctx := t.Context()
provider, model, apiKey, baseURL, err := resolveBrowserLLM(ctx, db, "tenant-1", "tenant-model-1")
@@ -302,11 +304,11 @@ func TestBrowser_PropagatesRuntimeError(t *testing.T) {
// Override the tenant LLM lookup so the test doesn't need a
// real DB.
prevLookup := tenantLLMLookupForTest
tenantLLMLookupForTest = func(tenantID, modelName, factory string) (string, string, error) {
return "sk-test", "https://api.openai.com/v1", nil
prevLookup := browserLLMLookupForTest
browserLLMLookupForTest = func(ctx context.Context, db *gorm.DB, tenantID, llmID string) (string, string, string, string, error) {
return "OpenAI", "gpt-4o", "sk-test", "https://api.openai.com/v1", nil
}
t.Cleanup(func() { tenantLLMLookupForTest = prevLookup })
t.Cleanup(func() { browserLLMLookupForTest = prevLookup })
c, _ := NewBrowserComponent(map[string]any{
"llm_id": "gpt-4o@OpenAI",
@@ -420,11 +422,11 @@ func TestBrowser_RunExtractRequestShape(t *testing.T) {
mock := &mockStagehandInvoker{rawJSON: `"ok"`}
withMockRuntime(t, mock)
prevLookup := tenantLLMLookupForTest
tenantLLMLookupForTest = func(tenantID, modelName, factory string) (string, string, error) {
return "sk-test", "https://api.openai.com/v1", nil
prevLookup := browserLLMLookupForTest
browserLLMLookupForTest = func(ctx context.Context, db *gorm.DB, tenantID, llmID string) (string, string, string, string, error) {
return "OpenAI", "gpt-4o", "sk-test", "https://api.openai.com/v1", nil
}
t.Cleanup(func() { tenantLLMLookupForTest = prevLookup })
t.Cleanup(func() { browserLLMLookupForTest = prevLookup })
c, _ := NewBrowserComponent(map[string]any{
"llm_id": "gpt-4o@OpenAI",
@@ -462,11 +464,11 @@ func TestBrowser_HeadlessPropagates(t *testing.T) {
mock := &mockStagehandInvoker{rawJSON: `"ok"`}
withMockRuntime(t, mock)
prevLookup := tenantLLMLookupForTest
tenantLLMLookupForTest = func(tenantID, modelName, factory string) (string, string, error) {
return "sk-test", "", nil
prevLookup := browserLLMLookupForTest
browserLLMLookupForTest = func(ctx context.Context, db *gorm.DB, tenantID, llmID string) (string, string, string, string, error) {
return "OpenAI", "gpt-4o", "sk-test", "", nil
}
t.Cleanup(func() { tenantLLMLookupForTest = prevLookup })
t.Cleanup(func() { browserLLMLookupForTest = prevLookup })
c, _ := NewBrowserComponent(map[string]any{
"llm_id": "gpt-4o@OpenAI",
@@ -494,11 +496,11 @@ func TestBrowser_OutputsShape(t *testing.T) {
mock := &mockStagehandInvoker{rawJSON: `"the agent's final message"`}
withMockRuntime(t, mock)
prevLookup := tenantLLMLookupForTest
tenantLLMLookupForTest = func(tenantID, modelName, factory string) (string, string, error) {
return "sk-test", "", nil
prevLookup := browserLLMLookupForTest
browserLLMLookupForTest = func(ctx context.Context, db *gorm.DB, tenantID, llmID string) (string, string, string, string, error) {
return "OpenAI", "gpt-4o", "sk-test", "", nil
}
t.Cleanup(func() { tenantLLMLookupForTest = prevLookup })
t.Cleanup(func() { browserLLMLookupForTest = prevLookup })
c, _ := NewBrowserComponent(map[string]any{
"llm_id": "gpt-4o@OpenAI",

View File

@@ -54,6 +54,8 @@ import (
"ragflow/internal/agent/canvas"
"ragflow/internal/agent/runtime"
"gorm.io/gorm"
)
// TestStagehandRuntime_Extract is the single happy-path integration
@@ -206,11 +208,11 @@ func TestBrowser_E2E_Extract(t *testing.T) {
t.Cleanup(srv.Close)
// Override tenant LLM lookup so the test doesn't need a real DB.
prevLookup := tenantLLMLookupForTest
tenantLLMLookupForTest = func(_, _, _ string) (string, string, error) {
return apiKey, baseURL, nil
prevLookup := browserLLMLookupForTest
browserLLMLookupForTest = func(_ context.Context, _ *gorm.DB, _, _ string) (string, string, string, string, error) {
return "OpenAI", model, apiKey, baseURL, nil
}
t.Cleanup(func() { tenantLLMLookupForTest = prevLookup })
t.Cleanup(func() { browserLLMLookupForTest = prevLookup })
// --- use production stagehand runtime ---
r := newStagehandRuntimeFromEnv()

View File

@@ -28,7 +28,7 @@ import { useState } from 'react';
function MyComponent() {
const [date, setDate] = useState<Date | undefined>(new Date());
return (
<Calendar
mode="single"

View File

@@ -29,12 +29,12 @@ import { useState } from 'react';
function MyComponent() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Open Modal</button>
<Modal
open={open}
<Modal
open={open}
onOpenChange={setOpen}
title="Modal Title"
>
@@ -193,8 +193,8 @@ Shows the basic modal with default size and standard header/footer.
const [open, setOpen] = useState(false);
<Button onClick={() => setOpen(true)}>Open Default Modal</Button>
<Modal
open={open}
<Modal
open={open}
onOpenChange={setOpen}
title="Default Modal"
>
@@ -257,8 +257,8 @@ Shows a small-sized modal, ideal for confirmations or brief messages.
const [open, setOpen] = useState(false);
<Button onClick={() => setOpen(true)}>Open Small Modal</Button>
<Modal
open={open}
<Modal
open={open}
onOpenChange={setOpen}
title="Small Modal"
size="small"
@@ -326,8 +326,8 @@ Shows a large-sized modal, suitable for complex content like forms or data table
const [open, setOpen] = useState(false);
<Button onClick={() => setOpen(true)}>Open Large Modal</Button>
<Modal
open={open}
<Modal
open={open}
onOpenChange={setOpen}
title="Large Modal"
size="large"
@@ -405,8 +405,8 @@ Shows a modal with a custom footer. You can provide your own footer content inst
const [open, setOpen] = useState(false);
<Button onClick={() => setOpen(true)}>Open Modal with Custom Footer</Button>
<Modal
open={open}
<Modal
open={open}
onOpenChange={setOpen}
title="Custom Footer"
footer={
@@ -477,8 +477,8 @@ Shows a modal without a footer. Useful when you want to include action buttons w
const [open, setOpen] = useState(false);
<Button onClick={() => setOpen(true)}>Open Modal without Footer</Button>
<Modal
open={open}
<Modal
open={open}
onOpenChange={setOpen}
title="No Footer"
showfooter={false}
@@ -544,8 +544,8 @@ Shows a full screen modal that takes up the entire viewport. Useful for complex
const [open, setOpen] = useState(false);
<Button onClick={() => setOpen(true)}>Open Full Screen Modal</Button>
<Modal
open={open}
<Modal
open={open}
onOpenChange={setOpen}
title="Full Screen Modal"
full={true}
@@ -630,8 +630,8 @@ const handleOk = () => {
};
<Button onClick={() => setOpen(true)}>Open Loading State Modal</Button>
<Modal
open={open}
<Modal
open={open}
onOpenChange={setOpen}
title="Loading State"
confirmLoading={loading}
@@ -705,12 +705,12 @@ import { Modal } from '@/components/ui/modal/modal';
function InteractiveModal() {
const [open, setOpen] = useState(false);
return (
<div>
<Button onClick={() => setOpen(true)}>Open Interactive Modal</Button>
<Modal
open={open}
<Modal
open={open}
onOpenChange={setOpen}
title="Interactive Modal"
onOk={() => {