mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-29 12:09:31 +08:00
Fix: rm tenant llm call (#17476)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -28,7 +28,7 @@ import { useState } from 'react';
|
||||
|
||||
function MyComponent() {
|
||||
const [date, setDate] = useState<Date | undefined>(new Date());
|
||||
|
||||
|
||||
return (
|
||||
<Calendar
|
||||
mode="single"
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
Reference in New Issue
Block a user