mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-08-28 04:50:16 +03:00
feat(docs): enhance LLM model configs API documentation with schema updates
- Add `model_type` field to database schema with supported values (text, vision, stt, tts, multimodal, embedding, reranking, other) - Add `name` field as table-level column for indexing and filtering - Add reserved JSONB fields for future extensibility - Update API request/response schemas to include `model_type` and `name` fields - Add new `LLMModelConfigWithSource` schema for detailed configuration responses - Update usage examples to reflect new required fields - Improve database constraints and indexing documentation
This commit is contained in:
parent
b968920a75
commit
d42fff49a5
@ -36,14 +36,31 @@ User's own config > User's group config
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `config_id` | UUID | Primary key |
|
||||
| `config` | JSONB | Configuration data (name, provider, model, etc.) |
|
||||
| `name` | VARCHAR(100) | Configuration name (table-level for indexing) |
|
||||
| `model_type` | VARCHAR(50) | Model type (table-level for filtering) |
|
||||
| `config` | JSONB | Configuration data (provider, base_url, model, temperature, api_key, etc.) |
|
||||
| `user_id` | UUID (nullable) | Foreign key to users table |
|
||||
| `group_id` | UUID (nullable) | Foreign key to user_groups table |
|
||||
| `is_default` | BOOLEAN | Default configuration flag |
|
||||
| `version` | INTEGER | Optimistic locking version (starts at 0, increments on each update) |
|
||||
| `reserved_jsonb_1` | JSONB (nullable) | Reserved field for future use |
|
||||
| `reserved_jsonb_2` | JSONB (nullable) | Reserved field for future use |
|
||||
| `reserved_jsonb_3` | JSONB (nullable) | Reserved field for future use |
|
||||
| `created_at` | TIMESTAMP | Creation timestamp |
|
||||
| `updated_at` | TIMESTAMP | Last update timestamp |
|
||||
|
||||
### Model Types
|
||||
|
||||
The `model_type` field accepts the following values:
|
||||
- `text` - Text generation models
|
||||
- `vision` - Vision/image understanding models
|
||||
- `stt` - Speech-to-Text models
|
||||
- `tts` - Text-to-Speech models
|
||||
- `multimodal` - Multimodal models supporting multiple input types
|
||||
- `embedding` - Text embedding models
|
||||
- `reranking` - Reranking models
|
||||
- `other` - Other model types
|
||||
|
||||
### Constraints
|
||||
|
||||
- Each config belongs to **either** a user **or** a group (not both)
|
||||
@ -87,6 +104,7 @@ User's own config > User's group config
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `name` | string | Configuration name (1-100 chars) |
|
||||
| `model_type` | string | Model type (text, vision, stt, tts, multimodal, embedding, reranking, other) |
|
||||
| `provider` | string | LLM provider (e.g., "openai", "anthropic", "ollama") |
|
||||
| `base_url` | string | API base URL |
|
||||
| `model` | string | Model name |
|
||||
@ -107,6 +125,7 @@ User's own config > User's group config
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `name` | string (optional) | Configuration name |
|
||||
| `model_type` | string (optional) | Model type |
|
||||
| `provider` | string (optional) | LLM provider |
|
||||
| `base_url` | string (optional) | API base URL |
|
||||
| `model` | string (optional) | Model name |
|
||||
@ -123,7 +142,9 @@ User's own config > User's group config
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `config_id` | UUID | Configuration ID |
|
||||
| `config` | LLMModelConfigData | Configuration data |
|
||||
| `name` | string | Configuration name |
|
||||
| `model_type` | string | Model type |
|
||||
| `config` | LLMModelConfigData | Configuration data (provider, base_url, model, temperature, etc.) |
|
||||
| `user_id` | UUID (nullable) | Owner user ID |
|
||||
| `group_id` | UUID (nullable) | Owner group ID |
|
||||
| `is_default` | boolean | Default flag |
|
||||
@ -139,6 +160,23 @@ User's own config > User's group config
|
||||
| `default_config` | LLMModelConfigWithSource (nullable) | Default configuration |
|
||||
| `total` | integer | Total count |
|
||||
|
||||
### LLMModelConfigWithSource
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `config_id` | UUID | Configuration ID |
|
||||
| `name` | string | Configuration name |
|
||||
| `model_type` | string | Model type |
|
||||
| `source` | string | Source: "user" or "group" |
|
||||
| `group_name` | string (nullable) | Group name if source is "group" |
|
||||
| `is_default` | boolean | Default flag |
|
||||
| `provider` | string | LLM provider |
|
||||
| `base_url` | string | API base URL |
|
||||
| `model` | string | Model name |
|
||||
| `temperature` | float | Temperature |
|
||||
| `api_key` | string (nullable) | API key |
|
||||
| `max_tokens` | integer (nullable) | Max tokens |
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
@ -151,6 +189,7 @@ curl -X POST http://localhost:3080/v3/access/users/{user_id}/llm-model-configs \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "GPT-4",
|
||||
"model_type": "text",
|
||||
"provider": "openai",
|
||||
"base_url": "https://api.openai.com/v1",
|
||||
"model": "gpt-4",
|
||||
@ -164,8 +203,9 @@ curl -X POST http://localhost:3080/v3/access/users/{user_id}/llm-model-configs \
|
||||
```json
|
||||
{
|
||||
"config_id": "uuid-1",
|
||||
"name": "GPT-4",
|
||||
"model_type": "text",
|
||||
"config": {
|
||||
"name": "GPT-4",
|
||||
"provider": "openai",
|
||||
"base_url": "https://api.openai.com/v1",
|
||||
"model": "gpt-4",
|
||||
@ -189,6 +229,7 @@ curl -X POST http://localhost:3080/v3/access/groups/{group_id}/llm-model-configs
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Claude-3",
|
||||
"model_type": "text",
|
||||
"provider": "anthropic",
|
||||
"base_url": "https://api.anthropic.com",
|
||||
"model": "claude-3-opus-20240229",
|
||||
@ -211,10 +252,11 @@ curl -X GET http://localhost:3080/v3/access/users/{user_id}/llm-model-configs \
|
||||
"configs": [
|
||||
{
|
||||
"config_id": "uuid-1",
|
||||
"name": "GPT-4",
|
||||
"model_type": "text",
|
||||
"source": "user",
|
||||
"group_name": null,
|
||||
"is_default": true,
|
||||
"name": "GPT-4",
|
||||
"provider": "openai",
|
||||
"model": "gpt-4",
|
||||
"base_url": "https://api.openai.com/v1",
|
||||
@ -224,10 +266,12 @@ curl -X GET http://localhost:3080/v3/access/users/{user_id}/llm-model-configs \
|
||||
],
|
||||
"default_config": {
|
||||
"config_id": "uuid-1",
|
||||
"name": "GPT-4",
|
||||
"model_type": "text",
|
||||
"source": "user",
|
||||
"group_name": null,
|
||||
"is_default": true,
|
||||
"name": "GPT-4",
|
||||
"provider": "openai",
|
||||
...
|
||||
},
|
||||
"total": 1
|
||||
@ -240,10 +284,11 @@ curl -X GET http://localhost:3080/v3/access/users/{user_id}/llm-model-configs \
|
||||
"configs": [
|
||||
{
|
||||
"config_id": "uuid-2",
|
||||
"name": "Claude-3",
|
||||
"model_type": "text",
|
||||
"source": "group",
|
||||
"group_name": "Developers",
|
||||
"is_default": true,
|
||||
"name": "Claude-3",
|
||||
"provider": "anthropic",
|
||||
"model": "claude-3-opus-20240229",
|
||||
"base_url": "https://api.anthropic.com",
|
||||
@ -253,6 +298,8 @@ curl -X GET http://localhost:3080/v3/access/users/{user_id}/llm-model-configs \
|
||||
],
|
||||
"default_config": {
|
||||
"config_id": "uuid-2",
|
||||
"name": "Claude-3",
|
||||
"model_type": "text",
|
||||
"source": "group",
|
||||
"group_name": "Developers",
|
||||
"is_default": true,
|
||||
@ -441,3 +488,59 @@ The old user settings API (`/v3/access/users/{user_id}/profiles`) stored configu
|
||||
- **Optimistic locking**: New `version` field and `expected_version` parameter
|
||||
- **Dedicated table**: Better query performance and data integrity
|
||||
- **Transparent encryption**: API keys auto-encrypted/decrypted by the API
|
||||
- **Model type support**: New `model_type` field for categorizing models (text, vision, stt, tts, multimodal, embedding, reranking, other)
|
||||
- **Table-level indexing**: `name` and `model_type` stored as table columns for efficient filtering and querying
|
||||
|
||||
---
|
||||
|
||||
## Model Type Filtering
|
||||
|
||||
The `model_type` table column enables efficient filtering and querying of configurations by model type:
|
||||
|
||||
### Common Use Cases
|
||||
|
||||
1. **Filter by model type**: Retrieve only text generation models for chat features
|
||||
2. **Multi-model applications**: Select appropriate model based on task type (text vs vision vs embedding)
|
||||
3. **Model type analytics**: Query and analyze usage patterns by model type
|
||||
4. **Type-specific defaults**: Set different default models for different model types
|
||||
|
||||
### Example: Filter text models (client-side)
|
||||
|
||||
```python
|
||||
# After fetching configs, filter by model_type
|
||||
configs = get_user_configs(user_id)
|
||||
text_models = [c for c in configs if c["model_type"] == "text"]
|
||||
vision_models = [c for c in configs if c["model_type"] == "vision"]
|
||||
```
|
||||
|
||||
### Database Index
|
||||
|
||||
The `model_type` column is indexed for efficient queries:
|
||||
```sql
|
||||
CREATE INDEX idx_llm_model_configs_model_type ON llm_model_configs(model_type);
|
||||
```
|
||||
|
||||
This enables fast lookups when filtering by model type, even with large datasets.
|
||||
|
||||
---
|
||||
|
||||
## Reserved Fields
|
||||
|
||||
The `llm_model_configs` table includes three reserved JSONB fields for future extensibility:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `reserved_jsonb_1` | JSONB (nullable) | Reserved for future use |
|
||||
| `reserved_jsonb_2` | JSONB (nullable) | Reserved for future use |
|
||||
| `reserved_jsonb_3` | JSONB (nullable) | Reserved for future use |
|
||||
|
||||
**Purpose:** These fields are reserved for future feature development without requiring schema changes. They are currently unused in the API code but are available in the database layer for future enhancements.
|
||||
|
||||
**Use Cases:** Future features might use these fields for:
|
||||
- Advanced configuration options
|
||||
- Metadata storage
|
||||
- Feature flags
|
||||
- Extension data
|
||||
- Caching computed values
|
||||
|
||||
**Note:** These fields are not exposed in the current API schemas and are reserved for internal use.
|
||||
|
||||
@ -97,6 +97,8 @@ async def get_user_own_llm_model_configs(
|
||||
return [
|
||||
schemas.LLMModelConfigResponse(
|
||||
config_id=config.config_id,
|
||||
name=config.name,
|
||||
model_type=config.model_type,
|
||||
config=config.config,
|
||||
user_id=config.user_id,
|
||||
group_id=config.group_id,
|
||||
@ -139,15 +141,20 @@ async def create_user_llm_model_config(
|
||||
raise ControllerNotFoundError(f"User '{user_id}' not found")
|
||||
|
||||
try:
|
||||
config_data = config_create.model_dump(exclude={"is_default"})
|
||||
# Extract config fields (excluding table-level fields)
|
||||
config_fields = config_create.model_dump(exclude={"name", "model_type", "is_default"})
|
||||
new_config = await llm_repo.create_user_config(
|
||||
user_id,
|
||||
config_data,
|
||||
config_create.name,
|
||||
config_create.model_type,
|
||||
config_fields,
|
||||
is_default=config_create.is_default
|
||||
)
|
||||
|
||||
return schemas.LLMModelConfigResponse(
|
||||
config_id=new_config.config_id,
|
||||
name=new_config.name,
|
||||
model_type=new_config.model_type,
|
||||
config=new_config.config,
|
||||
user_id=new_config.user_id,
|
||||
group_id=new_config.group_id,
|
||||
@ -206,6 +213,8 @@ async def update_user_llm_model_config(
|
||||
|
||||
return schemas.LLMModelConfigResponse(
|
||||
config_id=updated_config.config_id,
|
||||
name=updated_config.name,
|
||||
model_type=updated_config.model_type,
|
||||
config=updated_config.config,
|
||||
user_id=updated_config.user_id,
|
||||
group_id=updated_config.group_id,
|
||||
@ -293,6 +302,8 @@ async def set_user_default_llm_model_config(
|
||||
config = await llm_repo.get_user_config(config_id)
|
||||
return schemas.LLMModelConfigResponse(
|
||||
config_id=config.config_id,
|
||||
name=config.name,
|
||||
model_type=config.model_type,
|
||||
config=config.config,
|
||||
user_id=config.user_id,
|
||||
group_id=config.group_id,
|
||||
@ -335,6 +346,8 @@ async def get_group_llm_model_configs(
|
||||
return [
|
||||
schemas.LLMModelConfigResponse(
|
||||
config_id=config.config_id,
|
||||
name=config.name,
|
||||
model_type=config.model_type,
|
||||
config=config.config,
|
||||
user_id=config.user_id,
|
||||
group_id=config.group_id,
|
||||
@ -377,15 +390,20 @@ async def create_group_llm_model_config(
|
||||
raise ControllerNotFoundError(f"User group '{group_id}' not found")
|
||||
|
||||
try:
|
||||
config_data = config_create.model_dump(exclude={"is_default"})
|
||||
# Extract config fields (excluding table-level fields)
|
||||
config_fields = config_create.model_dump(exclude={"name", "model_type", "is_default"})
|
||||
new_config = await llm_repo.create_group_config(
|
||||
group_id,
|
||||
config_data,
|
||||
config_create.name,
|
||||
config_create.model_type,
|
||||
config_fields,
|
||||
is_default=config_create.is_default
|
||||
)
|
||||
|
||||
return schemas.LLMModelConfigResponse(
|
||||
config_id=new_config.config_id,
|
||||
name=new_config.name,
|
||||
model_type=new_config.model_type,
|
||||
config=new_config.config,
|
||||
user_id=new_config.user_id,
|
||||
group_id=new_config.group_id,
|
||||
@ -444,6 +462,8 @@ async def update_group_llm_model_config(
|
||||
|
||||
return schemas.LLMModelConfigResponse(
|
||||
config_id=updated_config.config_id,
|
||||
name=updated_config.name,
|
||||
model_type=updated_config.model_type,
|
||||
config=updated_config.config,
|
||||
user_id=updated_config.user_id,
|
||||
group_id=updated_config.group_id,
|
||||
@ -531,6 +551,8 @@ async def set_group_default_llm_model_config(
|
||||
config = await llm_repo.get_group_config(config_id)
|
||||
return schemas.LLMModelConfigResponse(
|
||||
config_id=config.config_id,
|
||||
name=config.name,
|
||||
model_type=config.model_type,
|
||||
config=config.config,
|
||||
user_id=config.user_id,
|
||||
group_id=config.group_id,
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from sqlalchemy import Column, Boolean, ForeignKey, CheckConstraint, UniqueConstraint, Index, Integer
|
||||
from sqlalchemy import Column, Boolean, ForeignKey, CheckConstraint, UniqueConstraint, Index, Integer, String
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
@ -35,12 +35,19 @@ class LLMModelConfig(BaseTable):
|
||||
__tablename__ = "llm_model_configs"
|
||||
|
||||
config_id = Column(GUID, primary_key=True, default=generate_uuid)
|
||||
config = Column(JSONB, nullable=False) # All config fields including name, provider, etc.
|
||||
name = Column(String(100), nullable=False) # Configuration name (table-level for indexing)
|
||||
model_type = Column(String(50), nullable=False) # Model type: text, vision, stt, tts, multimodal, etc.
|
||||
config = Column(JSONB, nullable=False) # Config fields: provider, base_url, model, temperature, api_key, etc.
|
||||
user_id = Column(GUID, ForeignKey("users.user_id", ondelete="CASCADE"), nullable=True)
|
||||
group_id = Column(GUID, ForeignKey("user_groups.user_group_id", ondelete="CASCADE"), nullable=True)
|
||||
is_default = Column(Boolean, default=False, nullable=False)
|
||||
version = Column(Integer, default=0, nullable=False) # Optimistic locking version
|
||||
|
||||
# Reserved fields for future use (currently unused in code)
|
||||
reserved_jsonb_1 = Column(JSONB, nullable=True)
|
||||
reserved_jsonb_2 = Column(JSONB, nullable=True)
|
||||
reserved_jsonb_3 = Column(JSONB, nullable=True)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", backref="llm_model_configs")
|
||||
group = relationship("UserGroup", backref="llm_model_configs")
|
||||
@ -53,6 +60,11 @@ class LLMModelConfig(BaseTable):
|
||||
"(user_id IS NULL AND group_id IS NOT NULL)",
|
||||
name="single_owner_check"
|
||||
),
|
||||
# Validate model_type values
|
||||
CheckConstraint(
|
||||
"model_type IN ('text', 'vision', 'stt', 'tts', 'multimodal', 'embedding', 'reranking', 'other')",
|
||||
name="valid_model_type_check"
|
||||
),
|
||||
# Each user can have at most one default config
|
||||
UniqueConstraint("user_id", "is_default", name="unique_user_default",
|
||||
deferrable=True, initially="deferred",
|
||||
@ -64,10 +76,10 @@ class LLMModelConfig(BaseTable):
|
||||
# Indexes for efficient queries
|
||||
Index("idx_llm_model_configs_user_id", "user_id"),
|
||||
Index("idx_llm_model_configs_group_id", "group_id"),
|
||||
Index("idx_llm_model_configs_model_type", "model_type"),
|
||||
Index("idx_llm_model_configs_config", "config", postgresql_using="gin"),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
config_name = self.config.get("name", "unnamed") if self.config else "unnamed"
|
||||
owner = f"user_{self.user_id}" if self.user_id else f"group_{self.group_id}"
|
||||
return f"<LLMModelConfig {config_name} for {owner} (default={self.is_default}, version={self.version})>"
|
||||
return f"<LLMModelConfig {self.name} ({self.model_type}) for {owner} (default={self.is_default}, version={self.version})>"
|
||||
|
||||
@ -68,6 +68,8 @@ class LLMModelConfigsRepository(BaseRepository):
|
||||
async def create_user_config(
|
||||
self,
|
||||
user_id: UUID,
|
||||
name: str,
|
||||
model_type: str,
|
||||
config_data: Dict[str, Any],
|
||||
is_default: bool = False
|
||||
) -> models.LLMModelConfig:
|
||||
@ -83,6 +85,8 @@ class LLMModelConfigsRepository(BaseRepository):
|
||||
raise
|
||||
|
||||
db_config = models.LLMModelConfig(
|
||||
name=name,
|
||||
model_type=model_type,
|
||||
config=config_to_store,
|
||||
user_id=user_id,
|
||||
is_default=is_default
|
||||
@ -130,21 +134,36 @@ class LLMModelConfigsRepository(BaseRepository):
|
||||
|
||||
# Encrypt API key if present in updates
|
||||
from gns3server.utils.encryption import encrypt
|
||||
updates_copy = updates.copy()
|
||||
if "api_key" in updates_copy and updates_copy["api_key"]:
|
||||
try:
|
||||
updates_copy["api_key"] = encrypt(updates_copy["api_key"])
|
||||
except Exception as e:
|
||||
log.error(f"Failed to encrypt API key: {e}")
|
||||
raise
|
||||
|
||||
# Update config JSONB fields
|
||||
# Table-level fields
|
||||
if "name" in updates and updates["name"] is not None:
|
||||
db_config.name = updates["name"]
|
||||
if "model_type" in updates and updates["model_type"] is not None:
|
||||
db_config.model_type = updates["model_type"]
|
||||
if "is_default" in updates and updates["is_default"] is not None:
|
||||
db_config.is_default = updates["is_default"]
|
||||
|
||||
# Config JSONB fields
|
||||
config_fields = ["provider", "base_url", "model", "temperature", "api_key", "max_tokens"]
|
||||
current_config = db_config.config.copy()
|
||||
for key, value in updates_copy.items():
|
||||
if key == "is_default":
|
||||
db_config.is_default = value
|
||||
elif value is not None:
|
||||
current_config[key] = value
|
||||
|
||||
for field in config_fields:
|
||||
if field in updates and updates[field] is not None:
|
||||
if field == "api_key":
|
||||
# Encrypt API key
|
||||
try:
|
||||
current_config[field] = encrypt(updates[field])
|
||||
except Exception as e:
|
||||
log.error(f"Failed to encrypt API key: {e}")
|
||||
raise
|
||||
else:
|
||||
current_config[field] = updates[field]
|
||||
|
||||
# Handle extra config fields
|
||||
for key, value in updates.items():
|
||||
if key not in ["name", "model_type", "is_default", "expected_version"] + config_fields:
|
||||
if value is not None:
|
||||
current_config[key] = value
|
||||
|
||||
db_config.config = current_config
|
||||
# Increment version for optimistic locking
|
||||
@ -225,6 +244,8 @@ class LLMModelConfigsRepository(BaseRepository):
|
||||
async def create_group_config(
|
||||
self,
|
||||
group_id: UUID,
|
||||
name: str,
|
||||
model_type: str,
|
||||
config_data: Dict[str, Any],
|
||||
is_default: bool = False
|
||||
) -> models.LLMModelConfig:
|
||||
@ -240,6 +261,8 @@ class LLMModelConfigsRepository(BaseRepository):
|
||||
raise
|
||||
|
||||
db_config = models.LLMModelConfig(
|
||||
name=name,
|
||||
model_type=model_type,
|
||||
config=config_to_store,
|
||||
group_id=group_id,
|
||||
is_default=is_default
|
||||
@ -287,21 +310,36 @@ class LLMModelConfigsRepository(BaseRepository):
|
||||
|
||||
# Encrypt API key if present in updates
|
||||
from gns3server.utils.encryption import encrypt
|
||||
updates_copy = updates.copy()
|
||||
if "api_key" in updates_copy and updates_copy["api_key"]:
|
||||
try:
|
||||
updates_copy["api_key"] = encrypt(updates_copy["api_key"])
|
||||
except Exception as e:
|
||||
log.error(f"Failed to encrypt API key: {e}")
|
||||
raise
|
||||
|
||||
# Update config JSONB fields
|
||||
# Table-level fields
|
||||
if "name" in updates and updates["name"] is not None:
|
||||
db_config.name = updates["name"]
|
||||
if "model_type" in updates and updates["model_type"] is not None:
|
||||
db_config.model_type = updates["model_type"]
|
||||
if "is_default" in updates and updates["is_default"] is not None:
|
||||
db_config.is_default = updates["is_default"]
|
||||
|
||||
# Config JSONB fields
|
||||
config_fields = ["provider", "base_url", "model", "temperature", "api_key", "max_tokens"]
|
||||
current_config = db_config.config.copy()
|
||||
for key, value in updates_copy.items():
|
||||
if key == "is_default":
|
||||
db_config.is_default = value
|
||||
elif value is not None:
|
||||
current_config[key] = value
|
||||
|
||||
for field in config_fields:
|
||||
if field in updates and updates[field] is not None:
|
||||
if field == "api_key":
|
||||
# Encrypt API key
|
||||
try:
|
||||
current_config[field] = encrypt(updates[field])
|
||||
except Exception as e:
|
||||
log.error(f"Failed to encrypt API key: {e}")
|
||||
raise
|
||||
else:
|
||||
current_config[field] = updates[field]
|
||||
|
||||
# Handle extra config fields
|
||||
for key, value in updates.items():
|
||||
if key not in ["name", "model_type", "is_default", "expected_version"] + config_fields:
|
||||
if value is not None:
|
||||
current_config[key] = value
|
||||
|
||||
db_config.config = current_config
|
||||
# Increment version for optimistic locking
|
||||
@ -392,6 +430,8 @@ class LLMModelConfigsRepository(BaseRepository):
|
||||
|
||||
configs_with_source.append({
|
||||
"config_id": config.config_id,
|
||||
"name": config.name,
|
||||
"model_type": config.model_type,
|
||||
"source": "user",
|
||||
"group_name": None,
|
||||
"is_default": config.is_default,
|
||||
@ -416,6 +456,8 @@ class LLMModelConfigsRepository(BaseRepository):
|
||||
|
||||
configs_with_source.append({
|
||||
"config_id": config.config_id,
|
||||
"name": config.name,
|
||||
"model_type": config.model_type,
|
||||
"source": "group",
|
||||
"group_name": group_names_map[group_id],
|
||||
"is_default": config.is_default,
|
||||
|
||||
@ -33,6 +33,8 @@ def upgrade() -> None:
|
||||
op.create_table(
|
||||
'llm_model_configs',
|
||||
sa.Column('config_id', postgresql.UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column('name', sa.String(100), nullable=False),
|
||||
sa.Column('model_type', sa.String(50), nullable=False),
|
||||
sa.Column('config', postgresql.JSONB(), nullable=False),
|
||||
sa.Column('user_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('users.user_id', ondelete='CASCADE'), nullable=True),
|
||||
sa.Column('group_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('user_groups.user_group_id', ondelete='CASCADE'), nullable=True),
|
||||
@ -40,11 +42,18 @@ def upgrade() -> None:
|
||||
sa.Column('version', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('reserved_jsonb_1', postgresql.JSONB(), nullable=True, comment='Reserved field for future use'),
|
||||
sa.Column('reserved_jsonb_2', postgresql.JSONB(), nullable=True, comment='Reserved field for future use'),
|
||||
sa.Column('reserved_jsonb_3', postgresql.JSONB(), nullable=True, comment='Reserved field for future use'),
|
||||
sa.CheckConstraint(
|
||||
"(user_id IS NOT NULL AND group_id IS NULL) OR "
|
||||
"(user_id IS NULL AND group_id IS NOT NULL)",
|
||||
name='single_owner_check'
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"model_type IN ('text', 'vision', 'stt', 'tts', 'multimodal', 'embedding', 'reranking', 'other')",
|
||||
name='valid_model_type_check'
|
||||
),
|
||||
sa.UniqueConstraint(
|
||||
'user_id', 'is_default',
|
||||
name='unique_user_default',
|
||||
@ -62,12 +71,14 @@ def upgrade() -> None:
|
||||
# Create indexes for efficient queries
|
||||
op.create_index('idx_llm_model_configs_user_id', 'llm_model_configs', ['user_id'])
|
||||
op.create_index('idx_llm_model_configs_group_id', 'llm_model_configs', ['group_id'])
|
||||
op.create_index('idx_llm_model_configs_model_type', 'llm_model_configs', ['model_type'])
|
||||
op.create_index('idx_llm_model_configs_config', 'llm_model_configs', ['config'], postgresql_using='gin')
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Drop indexes
|
||||
op.drop_index('idx_llm_model_configs_config', table_name='llm_model_configs')
|
||||
op.drop_index('idx_llm_model_configs_model_type', table_name='llm_model_configs')
|
||||
op.drop_index('idx_llm_model_configs_group_id', table_name='llm_model_configs')
|
||||
op.drop_index('idx_llm_model_configs_user_id', table_name='llm_model_configs')
|
||||
|
||||
|
||||
@ -14,21 +14,24 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Optional, Dict, Any
|
||||
from typing import Optional, List, Literal
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
from uuid import UUID
|
||||
|
||||
from .base import DateTimeModelMixin
|
||||
|
||||
|
||||
# Valid model types
|
||||
ModelType = Literal['text', 'vision', 'stt', 'tts', 'multimodal', 'embedding', 'reranking', 'other']
|
||||
|
||||
|
||||
# Core model config schema (stored in config JSONB field)
|
||||
class LLMModelConfigData(BaseModel):
|
||||
"""
|
||||
LLM model configuration data.
|
||||
All fields are stored in the config JSONB column.
|
||||
Stored in the config JSONB column (provider, base_url, model, etc.).
|
||||
"""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=100, description="Configuration name")
|
||||
provider: str = Field(..., description="LLM provider (e.g., 'openai', 'anthropic', 'ollama')")
|
||||
base_url: str = Field(..., description="API base URL")
|
||||
model: str = Field(..., description="Model name")
|
||||
@ -41,26 +44,42 @@ class LLMModelConfigData(BaseModel):
|
||||
|
||||
|
||||
# Request schemas
|
||||
class LLMModelConfigCreate(LLMModelConfigData):
|
||||
class LLMModelConfigCreate(BaseModel):
|
||||
"""Request to create a new LLM model configuration."""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=100, description="Configuration name")
|
||||
model_type: ModelType = Field(..., description="Model type")
|
||||
is_default: Optional[bool] = Field(False, description="Set as default configuration")
|
||||
# Config fields
|
||||
provider: str = Field(..., description="LLM provider")
|
||||
base_url: str = Field(..., description="API base URL")
|
||||
model: str = Field(..., description="Model name")
|
||||
temperature: float = Field(default=0.7, ge=0.0, le=2.0)
|
||||
api_key: Optional[str] = None
|
||||
max_tokens: Optional[int] = Field(None, gt=0)
|
||||
|
||||
# Allow extra config fields
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
||||
class LLMModelConfigUpdate(BaseModel):
|
||||
"""Request to update an existing LLM model configuration."""
|
||||
|
||||
# Table-level fields
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||
model_type: Optional[ModelType] = None
|
||||
is_default: Optional[bool] = None
|
||||
expected_version: Optional[int] = Field(None, description="Expected version for optimistic locking")
|
||||
|
||||
# Config fields
|
||||
provider: Optional[str] = None
|
||||
base_url: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
temperature: Optional[float] = Field(None, ge=0.0, le=2.0)
|
||||
api_key: Optional[str] = None
|
||||
max_tokens: Optional[int] = Field(None, gt=0)
|
||||
is_default: Optional[bool] = None
|
||||
expected_version: Optional[int] = Field(None, description="Expected version for optimistic locking")
|
||||
|
||||
# Allow extra fields for extensibility
|
||||
# Allow extra config fields
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
||||
@ -69,6 +88,8 @@ class LLMModelConfigResponse(DateTimeModelMixin):
|
||||
"""LLM model configuration response."""
|
||||
|
||||
config_id: UUID
|
||||
name: str
|
||||
model_type: ModelType
|
||||
config: LLMModelConfigData
|
||||
user_id: Optional[UUID] = None
|
||||
group_id: Optional[UUID] = None
|
||||
@ -78,22 +99,25 @@ class LLMModelConfigResponse(DateTimeModelMixin):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class LLMModelConfigListResponse(BaseModel):
|
||||
"""Response containing list of LLM model configurations."""
|
||||
|
||||
configs: list[LLMModelConfigData]
|
||||
default_config_id: Optional[UUID] = None
|
||||
total: int
|
||||
|
||||
|
||||
# Inheritance response (user configs + inherited group configs)
|
||||
class LLMModelConfigWithSource(LLMModelConfigData):
|
||||
"""Model configuration with source information."""
|
||||
class LLMModelConfigWithSource(BaseModel):
|
||||
"""Model configuration with source information (for inheritance)."""
|
||||
|
||||
config_id: UUID
|
||||
name: str
|
||||
model_type: ModelType
|
||||
source: str = Field(..., description="Source: 'user' or 'group'")
|
||||
group_name: Optional[str] = Field(None, description="Group name if source is 'group'")
|
||||
is_default: bool
|
||||
# Config fields
|
||||
provider: str
|
||||
base_url: str
|
||||
model: str
|
||||
temperature: float
|
||||
api_key: Optional[str] = None
|
||||
max_tokens: Optional[int] = None
|
||||
|
||||
# Allow extra config fields
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
||||
class LLMModelConfigInheritedResponse(BaseModel):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user