Files
ragflow/internal/dao/model_context.go
jay77721 91fd114783 fix(dao): honor tenant-configured context window override (D22/D23) (#18171)
Make `ResolveModelContentLength` honor the per-model custom **context window length** (`content_length`) — stored in the Python-legacy `tenant_model.extra["max_tokens"]` field, whose semantic meaning is the context window, NOT the generation cap — **before** any provider-catalog read, and remove the parallel service-layer implementation so every consumer shares one resolution path.
2026-08-12 19:32:37 +08:00

184 lines
7.1 KiB
Go

//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package dao
import (
"context"
"encoding/json"
"strconv"
"strings"
"gorm.io/gorm"
"ragflow/internal/entity"
)
// ResolveModelContentLength returns the chat model's effective context window
// (content_length) in tokens for modelRef — a tenant_model UUID or a
// composite "model@provider" / "model@instance@provider" reference — or 0
// when it cannot be resolved. tenantID scopes the composite-reference lookup
// to the tenant's own provider/instance/model rows. driver and modelName are
// an optional provider catalog fallback used when modelRef is not a
// resolvable tenant id (for example when no database is available); pass
// empty strings to skip it.
//
// Resolution order (mirrors Python's model_extra.get("max_tokens") semantics):
// 1. Tenant configuration first: when the tenant_model row is active and
// carries a positive "max_tokens" override in its extra JSON, that custom
// context window wins and no catalog data is read.
// 2. Only with no override, fall back to the provider catalog's
// content_length (via the tenant row, the composite reference parts, or
// the resolved driver + model name).
//
// This is the shared implementation behind the agent LLM component, the
// ingestion Extractor, and service.ModelProviderService — those packages
// cannot import each other (import cycles), so the lookup lives here.
func ResolveModelContentLength(ctx context.Context, db *gorm.DB, tenantID, modelRef, driver, modelName string) int {
if db == nil {
db = DB
}
pureName, instanceName, providerName, composite := splitCompositeModelRef(modelRef)
// 1. Tenant-configured override: resolve the tenant_model row before any
// catalog read and return the custom context window when present.
obj := lookupTenantModel(ctx, db, tenantID, modelRef, pureName, instanceName, providerName, composite)
if obj != nil && obj.Status == "active" {
if v := modelExtraMaxTokens(obj.Extra); v > 0 {
return v
}
}
// 2. Catalog fallbacks (only when there is no custom override).
// 2a. Active tenant row → its provider row → catalog content_length.
if obj != nil && obj.Status == "active" {
if provider, err := NewTenantModelProviderDAO().GetByID(ctx, db, obj.ProviderID); err == nil && provider != nil {
if mdl, err := GetModelProviderManager().GetModelByName(provider.ProviderName, obj.ModelName); err == nil && mdl.ContentLength != nil {
return *mdl.ContentLength
}
}
}
// 2b. Composite reference with no tenant row → catalog by reference parts.
if composite {
if mdl, err := GetModelProviderManager().GetModelByName(providerName, pureName); err == nil && mdl.ContentLength != nil {
return *mdl.ContentLength
}
}
// 2c. Resolved driver + bare model name: fallback when modelRef is a
// tenant id that could not be resolved without a database.
if driver != "" && modelName != "" {
if mdl, err := GetModelProviderManager().GetModelByName(driver, modelName); err == nil && mdl.ContentLength != nil {
return *mdl.ContentLength
}
}
return 0
}
// lookupTenantModel resolves the tenant_model row for modelRef — by UUID, or
// for a composite reference through the tenant's provider/instance rows (chat
// model type). Returns nil when the row cannot be resolved.
func lookupTenantModel(ctx context.Context, db *gorm.DB, tenantID, modelRef, pureName, instanceName, providerName string, composite bool) *entity.TenantModel {
if db == nil {
return nil
}
if composite {
if tenantID == "" {
return nil
}
provider, err := NewTenantModelProviderDAO().GetByTenantIDAndProviderName(ctx, db, tenantID, providerName)
if err != nil || provider == nil {
return nil
}
instance, err := NewTenantModelInstanceDAO().GetByProviderIDAndInstanceName(ctx, db, provider.ID, instanceName)
if err != nil || instance == nil || instance.Status != "active" {
return nil
}
obj, err := NewTenantModelDAO().GetByProviderIDAndInstanceIDAndModelTypeAndModelName(ctx, db, provider.ID, instance.ID, int(entity.ModelTypeChat), pureName)
if err != nil || obj == nil {
return nil
}
return obj
}
if modelRef == "" {
return nil
}
obj, err := NewTenantModelDAO().GetByID(ctx, db, modelRef)
if err != nil || obj == nil {
return nil
}
// UUIDs are globally unique and unguessable, and shared/joined-tenant
// models are legitimate (Python's get_model_config_by_id and Go's
// service.GetModelConfigByID both support them), so UUID resolution is
// intentionally NOT scoped to tenantID. The per-model override and the
// catalog fallback apply to whichever tenant holds the UUID.
return obj
}
// modelExtraMaxTokens returns the per-model "max_tokens" override from the
// tenant_model.extra JSON, or 0 when absent/invalid/non-positive. Mirrors
// Python's model_extra.get("max_tokens") — the custom context-window length a
// user configures for a model. Both JSON numbers and numeric strings are
// accepted (some callers persist extra.max_tokens as a string).
func modelExtraMaxTokens(extra string) int {
if strings.TrimSpace(extra) == "" {
return 0
}
var m struct {
MaxTokens json.RawMessage `json:"max_tokens"`
}
if err := json.Unmarshal([]byte(extra), &m); err != nil || len(m.MaxTokens) == 0 {
return 0
}
// Accept JSON numbers (int or float, e.g. 32000 / 32000.0) and numeric
// strings ("32000"); json.Number keeps the raw lexical form.
var num json.Number
if err := json.Unmarshal(m.MaxTokens, &num); err == nil {
if f, err := strconv.ParseFloat(num.String(), 64); err == nil && f > 0 {
return int(f)
}
return 0
}
var str string
if err := json.Unmarshal(m.MaxTokens, &str); err == nil {
if n, err := strconv.Atoi(strings.TrimSpace(str)); err == nil && n > 0 {
return n
}
}
return 0
}
// splitCompositeModelRef splits a composite "model@provider" or
// "model@instance@provider" reference into its parts. The instance defaults
// to "default" for the two-part form.
func splitCompositeModelRef(ref string) (modelName, instanceName, providerName string, ok bool) {
parts := strings.Split(ref, "@")
switch len(parts) {
case 2:
return parts[0], "default", parts[1], true
case 3:
return parts[0], parts[1], parts[2], true
}
if len(parts) > 3 {
// 4+ segments: any '@' embedded in the leftmost modelName component
// must be preserved (e.g. LM Studio chat models
// `name@q8_0@lmstudio@LM-Studio`). Rejoin the leading fields into the
// model name, keeping instance and provider anchored on the right.
n := len(parts)
return strings.Join(parts[:n-2], "@"), parts[n-2], parts[n-1], true
}
return "", "", "", false
}