From d42fff49a5cd5ab3e561ff90d624ba80c26f2ffc Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Tue, 3 Mar 2026 17:54:30 +0800 Subject: [PATCH] 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 --- docs/llm-model-configs-api.md | 115 +++++++++++++++++- .../routes/controller/llm_model_configs.py | 30 ++++- gns3server/db/models/llm_model_configs.py | 20 ++- .../db/repositories/llm_model_configs.py | 94 ++++++++++---- ...20260303_create_llm_model_configs_table.py | 11 ++ .../schemas/controller/llm_model_configs.py | 60 ++++++--- 6 files changed, 272 insertions(+), 58 deletions(-) diff --git a/docs/llm-model-configs-api.md b/docs/llm-model-configs-api.md index 608875fd5..8fa1144f8 100644 --- a/docs/llm-model-configs-api.md +++ b/docs/llm-model-configs-api.md @@ -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. diff --git a/gns3server/api/routes/controller/llm_model_configs.py b/gns3server/api/routes/controller/llm_model_configs.py index dc19761c6..01bd95ba2 100644 --- a/gns3server/api/routes/controller/llm_model_configs.py +++ b/gns3server/api/routes/controller/llm_model_configs.py @@ -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, diff --git a/gns3server/db/models/llm_model_configs.py b/gns3server/db/models/llm_model_configs.py index c4b128a73..2656ed83a 100644 --- a/gns3server/db/models/llm_model_configs.py +++ b/gns3server/db/models/llm_model_configs.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -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"" + return f"" diff --git a/gns3server/db/repositories/llm_model_configs.py b/gns3server/db/repositories/llm_model_configs.py index c5eb110dc..6d340f89f 100644 --- a/gns3server/db/repositories/llm_model_configs.py +++ b/gns3server/db/repositories/llm_model_configs.py @@ -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, diff --git a/gns3server/db_migrations/versions/20260303_create_llm_model_configs_table.py b/gns3server/db_migrations/versions/20260303_create_llm_model_configs_table.py index b9d6dae13..007bde3b1 100644 --- a/gns3server/db_migrations/versions/20260303_create_llm_model_configs_table.py +++ b/gns3server/db_migrations/versions/20260303_create_llm_model_configs_table.py @@ -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') diff --git a/gns3server/schemas/controller/llm_model_configs.py b/gns3server/schemas/controller/llm_model_configs.py index 17525e37c..87fe8d24d 100644 --- a/gns3server/schemas/controller/llm_model_configs.py +++ b/gns3server/schemas/controller/llm_model_configs.py @@ -14,21 +14,24 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -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):