alloy-runtime-types 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. alloy_runtime_types-0.1.0/PKG-INFO +35 -0
  2. alloy_runtime_types-0.1.0/README.md +18 -0
  3. alloy_runtime_types-0.1.0/alloy_runtime_types/__init__.py +1 -0
  4. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/__init__.py +1 -0
  5. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/admin.py +25 -0
  6. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/agents.py +362 -0
  7. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/api_keys.py +124 -0
  8. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/audio.py +9 -0
  9. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/auth.py +132 -0
  10. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/billing.py +141 -0
  11. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/content.py +245 -0
  12. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/credentials.py +277 -0
  13. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/generation.py +644 -0
  14. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/knowledge.py +1221 -0
  15. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/mcp_servers.py +258 -0
  16. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/messages.py +80 -0
  17. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/models.py +54 -0
  18. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/organization_credentials.py +95 -0
  19. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/organizations.py +30 -0
  20. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/pipeline.py +336 -0
  21. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/pipeline_costs.py +108 -0
  22. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/render.py +54 -0
  23. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/schedule.py +446 -0
  24. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/schemas.py +194 -0
  25. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/sessions.py +197 -0
  26. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/structured_filter.py +70 -0
  27. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/tags.py +101 -0
  28. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/templates.py +273 -0
  29. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/tool_configs.py +120 -0
  30. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/tools.py +24 -0
  31. alloy_runtime_types-0.1.0/alloy_runtime_types/dtos/users.py +70 -0
  32. alloy_runtime_types-0.1.0/alloy_runtime_types/enums/__init__.py +1 -0
  33. alloy_runtime_types-0.1.0/alloy_runtime_types/enums/agent_enums.py +12 -0
  34. alloy_runtime_types-0.1.0/alloy_runtime_types/enums/api_key_scope.py +33 -0
  35. alloy_runtime_types-0.1.0/alloy_runtime_types/enums/content_enums.py +24 -0
  36. alloy_runtime_types-0.1.0/alloy_runtime_types/enums/member_role.py +12 -0
  37. alloy_runtime_types-0.1.0/alloy_runtime_types/enums/ownership_scope.py +9 -0
  38. alloy_runtime_types-0.1.0/alloy_runtime_types/enums/provider.py +102 -0
  39. alloy_runtime_types-0.1.0/alloy_runtime_types/enums/schema_enums.py +8 -0
  40. alloy_runtime_types-0.1.0/alloy_runtime_types/enums/template_enums.py +19 -0
  41. alloy_runtime_types-0.1.0/alloy_runtime_types/enums/tool_service.py +50 -0
  42. alloy_runtime_types-0.1.0/alloy_runtime_types/parsing/__init__.py +0 -0
  43. alloy_runtime_types-0.1.0/alloy_runtime_types/parsing/filter_parser.py +49 -0
  44. alloy_runtime_types-0.1.0/alloy_runtime_types.egg-info/PKG-INFO +35 -0
  45. alloy_runtime_types-0.1.0/alloy_runtime_types.egg-info/SOURCES.txt +52 -0
  46. alloy_runtime_types-0.1.0/alloy_runtime_types.egg-info/dependency_links.txt +1 -0
  47. alloy_runtime_types-0.1.0/alloy_runtime_types.egg-info/requires.txt +1 -0
  48. alloy_runtime_types-0.1.0/alloy_runtime_types.egg-info/top_level.txt +1 -0
  49. alloy_runtime_types-0.1.0/pyproject.toml +43 -0
  50. alloy_runtime_types-0.1.0/setup.cfg +4 -0
  51. alloy_runtime_types-0.1.0/tests/test_api_key_dto.py +232 -0
  52. alloy_runtime_types-0.1.0/tests/test_audio_dto.py +48 -0
  53. alloy_runtime_types-0.1.0/tests/test_mcp_servers_dto.py +337 -0
  54. alloy_runtime_types-0.1.0/tests/test_pipeline_dto.py +39 -0
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: alloy-runtime-types
3
+ Version: 0.1.0
4
+ Summary: Type definitions and DTOs for Alloy Runtime
5
+ Author-email: Grant Jordan <gjordan1997@gmail.com>
6
+ Project-URL: Repository, https://github.com/alloy-runtime/alloy-runtime
7
+ Project-URL: Issues, https://github.com/alloy-runtime/alloy-runtime/issues
8
+ Keywords: alloy,api,dto,python,types
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Requires-Python: >=3.13
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: pydantic>=2.12.3
17
+
18
+ # alloy-runtime-types
19
+
20
+ Shared DTOs, enums, and parsing helpers for Alloy Runtime Python clients.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install alloy-runtime-types
26
+ ```
27
+
28
+ ## What it includes
29
+
30
+ - Pydantic request and response models used by the SDK and CLI.
31
+ - Shared enums for ownership scope, providers, and tool metadata.
32
+ - Parsing helpers used by higher-level packages.
33
+
34
+ This package is published separately so `alloy-runtime-sdk` and `alloy-runtime-cli`
35
+ can be installed cleanly from PyPI without a workspace checkout.
@@ -0,0 +1,18 @@
1
+ # alloy-runtime-types
2
+
3
+ Shared DTOs, enums, and parsing helpers for Alloy Runtime Python clients.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install alloy-runtime-types
9
+ ```
10
+
11
+ ## What it includes
12
+
13
+ - Pydantic request and response models used by the SDK and CLI.
14
+ - Shared enums for ownership scope, providers, and tool metadata.
15
+ - Parsing helpers used by higher-level packages.
16
+
17
+ This package is published separately so `alloy-runtime-sdk` and `alloy-runtime-cli`
18
+ can be installed cleanly from PyPI without a workspace checkout.
@@ -0,0 +1 @@
1
+ # Empty per project convention - import directly from submodules
@@ -0,0 +1 @@
1
+ # Empty per project convention - import directly from submodules
@@ -0,0 +1,25 @@
1
+ """DTOs for admin operations."""
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class SyncModelsRequest(BaseModel):
7
+ """Request to sync AI models from external providers."""
8
+
9
+ pass
10
+
11
+
12
+ class ProviderSyncResult(BaseModel):
13
+ """Results from syncing a single provider source."""
14
+
15
+ models_synced: int
16
+ providers_synced: int
17
+ error: str | None = None
18
+
19
+
20
+ class SyncModelsResponse(BaseModel):
21
+ """Aggregated sync results from all provider sources."""
22
+
23
+ total_models_synced: int
24
+ total_providers_synced: int
25
+ by_source: dict[str, ProviderSyncResult]
@@ -0,0 +1,362 @@
1
+ """DTOs for agent operations."""
2
+
3
+ from datetime import datetime
4
+ from typing import Any
5
+ from uuid import UUID
6
+
7
+ from pydantic import BaseModel, Field, model_validator
8
+ from typing_extensions import Self
9
+
10
+ from alloy_runtime_types.dtos.generation import GenerationParameters
11
+ from alloy_runtime_types.dtos.mcp_servers import (
12
+ AgentMCPServerConfig,
13
+ AgentMCPServerResponse,
14
+ )
15
+ from alloy_runtime_types.enums.ownership_scope import OwnershipScope
16
+ from alloy_runtime_types.enums.provider import Provider
17
+
18
+
19
+ class AgentModelConfig(BaseModel):
20
+ """Configuration for an agent model."""
21
+
22
+ provider_key: Provider = Field(min_length=1)
23
+ provider_model_name: str = Field(min_length=1)
24
+ preference_order: int = Field(default=0, ge=0)
25
+
26
+
27
+ class AgentToolConfig(BaseModel):
28
+ """Configuration for an agent tool.
29
+
30
+ Configuration can come from either:
31
+ - Inline config (config field): Direct configuration dict
32
+ - Config reference (tool_config_id): Reference to a reusable ToolConfig
33
+
34
+ These are mutually exclusive - at most one can be set.
35
+
36
+ The tool_alias enables multiple instances of the same tool per agent.
37
+ It defaults to tool_id when not provided and is used as the OpenAI function name.
38
+ """
39
+
40
+ tool_id: str = Field(min_length=1)
41
+ tool_alias: str | None = Field(
42
+ default=None,
43
+ min_length=1,
44
+ description="Unique alias for this tool instance within the agent. "
45
+ "Used as the OpenAI function name. Defaults to tool_id if not provided.",
46
+ )
47
+ org_credential_id: UUID | None = None
48
+ is_enabled: bool = True
49
+ tool_order: int = Field(default=0, ge=0)
50
+ config: dict[str, Any] | None = Field(
51
+ default=None,
52
+ description="Inline tool configuration. Validated against tool's parameter_schema.",
53
+ )
54
+ tool_config_id: UUID | None = Field(
55
+ default=None,
56
+ description="Reference to a reusable ToolConfig. Mutually exclusive with config.",
57
+ )
58
+ llm_instruction: str | None = Field(
59
+ default=None,
60
+ description="Optional instruction appended to tool description for LLM. "
61
+ "Provides per-agent hints about when/how to use this tool.",
62
+ )
63
+
64
+ @model_validator(mode="after")
65
+ def validate_config_exclusivity(self) -> Self:
66
+ """Ensure config and tool_config_id are mutually exclusive."""
67
+ if self.config is not None and self.tool_config_id is not None:
68
+ raise ValueError(
69
+ "Cannot specify both config and tool_config_id. Provide one or neither."
70
+ )
71
+ return self
72
+
73
+ @property
74
+ def effective_alias(self) -> str:
75
+ """Return tool_alias if set, otherwise tool_id."""
76
+ return self.tool_alias if self.tool_alias else self.tool_id
77
+
78
+
79
+ class CreateAgentRequest(BaseModel):
80
+ """Request to create a new agent."""
81
+
82
+ name: str = Field(min_length=1)
83
+ models: list[AgentModelConfig] = Field(min_length=1)
84
+ tags: list[str] = Field(
85
+ default_factory=list,
86
+ description="List of tag paths to associate with this agent. Required for user and organization scoped agents. Must be empty for global agents.",
87
+ )
88
+ scope: OwnershipScope = Field(default=OwnershipScope.USER)
89
+ base_agent_id: UUID | None = None
90
+ description: str | None = None
91
+ system_instruction_template: str | UUID | None = Field(
92
+ default=None,
93
+ description="System instruction template identifier - can be UUID or template name. "
94
+ "Names are resolved with priority: user-owned > org-owned > global.",
95
+ )
96
+ system_instruction_version_id: UUID | None = None
97
+ input_schema_id: UUID | None = None
98
+ output_schema_id: UUID | None = None
99
+ output_schema: dict[str, Any] | None = None
100
+ default_request_headers: dict[str, str] | None = None
101
+ generation_params: GenerationParameters | None = Field(
102
+ default=None,
103
+ description="Default generation parameters for this agent",
104
+ )
105
+ tools: list[AgentToolConfig] | None = None
106
+ mcp_servers: list[AgentMCPServerConfig] | None = None
107
+
108
+ @model_validator(mode="after")
109
+ def validate_output_schema_exclusivity(self) -> Self:
110
+ """Ensure output_schema_id and output_schema are mutually exclusive."""
111
+ if self.output_schema_id is not None and self.output_schema is not None:
112
+ raise ValueError(
113
+ "Cannot specify both output_schema_id and output_schema. "
114
+ "Provide one or neither."
115
+ )
116
+ return self
117
+
118
+ @model_validator(mode="after")
119
+ def validate_tool_alias_uniqueness(self) -> Self:
120
+ """Ensure tool_alias is unique within the tools list."""
121
+ if self.tools:
122
+ aliases = [t.effective_alias for t in self.tools]
123
+ if len(aliases) != len(set(aliases)):
124
+ raise ValueError(
125
+ "Each tool must have a unique tool_alias. "
126
+ "Duplicate aliases found in tools list."
127
+ )
128
+ return self
129
+
130
+ @model_validator(mode="after")
131
+ def validate_mcp_server_id_uniqueness(self) -> Self:
132
+ """Ensure mcp_server_id is unique within the mcp_servers list."""
133
+ if self.mcp_servers:
134
+ ids = [m.mcp_server_id for m in self.mcp_servers]
135
+ if len(ids) != len(set(ids)):
136
+ raise ValueError(
137
+ "Duplicate mcp_server_id found in mcp_servers list. "
138
+ "Each MCP server can only be attached once."
139
+ )
140
+ return self
141
+
142
+
143
+ class AgentModelResponse(BaseModel):
144
+ """Response model for agent model configuration."""
145
+
146
+ agent_id: UUID
147
+ provider_key: Provider
148
+ provider_model_name: str
149
+ preference_order: int
150
+
151
+
152
+ class AgentToolResponse(BaseModel):
153
+ """Response model for agent tool configuration."""
154
+
155
+ agent_id: UUID
156
+ tool_alias: str = Field(description="Unique alias for this tool within the agent")
157
+ tool_id: str = Field(description="Tool implementation ID (references ai.tools.id)")
158
+ org_credential_id: UUID | None
159
+ is_enabled: bool
160
+ tool_order: int
161
+ config: dict[str, Any] | None = None
162
+ tool_config_id: UUID | None = None
163
+ tool_config_name: str | None = Field(
164
+ default=None,
165
+ description="Name of the referenced ToolConfig (convenience field for display)",
166
+ )
167
+ llm_instruction: str | None = Field(
168
+ default=None,
169
+ description="Optional instruction appended to tool description for LLM",
170
+ )
171
+ created_at: datetime
172
+
173
+
174
+ class AgentTagResponse(BaseModel):
175
+ """Response model for agent tag."""
176
+
177
+ tag_id: UUID
178
+ tag_path: str
179
+ display_name: str
180
+
181
+
182
+ class CreateAgentResponse(BaseModel):
183
+ """Response after creating an agent."""
184
+
185
+ id: UUID
186
+ name: str
187
+ organization_id: UUID | None
188
+ user_id: UUID | None
189
+ base_agent_id: UUID | None
190
+ description: str | None
191
+ system_instruction_version_id: UUID | None
192
+ input_schema_id: UUID | None
193
+ output_schema_id: UUID | None
194
+ default_request_headers: dict[str, str] | None
195
+ generation_params: GenerationParameters | None = None
196
+ created_at: datetime
197
+ updated_at: datetime
198
+ models: list[AgentModelResponse]
199
+ tools: list[AgentToolResponse]
200
+ mcp_servers: list[AgentMCPServerResponse] = []
201
+ tags: list[AgentTagResponse]
202
+
203
+
204
+ class UpdateAgentRequest(BaseModel):
205
+ """Request to update an existing agent.
206
+
207
+ All fields are optional - None means no change to that field.
208
+ """
209
+
210
+ name: str | None = Field(default=None, min_length=1)
211
+ description: str | None = None
212
+ models: list[AgentModelConfig] | None = None
213
+ tags: list[str] | None = Field(
214
+ default=None,
215
+ description="List of tag paths to associate with this agent. If provided, replaces all existing tags.",
216
+ )
217
+ system_instruction_template: str | UUID | None = Field(
218
+ default=None,
219
+ description="System instruction template identifier - can be UUID or template name. "
220
+ "Names are resolved with priority: user-owned > org-owned > global.",
221
+ )
222
+ system_instruction_version_id: UUID | None = None
223
+ input_schema_id: UUID | None = None
224
+ output_schema_id: UUID | None = None
225
+ output_schema: dict[str, Any] | None = None
226
+ default_request_headers: dict[str, str] | None = None
227
+ generation_params: GenerationParameters | None = Field(
228
+ default=None,
229
+ description="Default generation parameters for this agent",
230
+ )
231
+ tools: list[AgentToolConfig] | None = None
232
+ mcp_servers: list[AgentMCPServerConfig] | None = None
233
+
234
+ @model_validator(mode="after")
235
+ def validate_output_schema_exclusivity(self) -> Self:
236
+ """Ensure output_schema_id and output_schema are mutually exclusive."""
237
+ if self.output_schema_id is not None and self.output_schema is not None:
238
+ raise ValueError(
239
+ "Cannot specify both output_schema_id and output_schema. "
240
+ "Provide one or neither."
241
+ )
242
+ return self
243
+
244
+ @model_validator(mode="after")
245
+ def validate_tool_alias_uniqueness(self) -> Self:
246
+ """Ensure tool_alias is unique within the tools list."""
247
+ if self.tools:
248
+ aliases = [t.effective_alias for t in self.tools]
249
+ if len(aliases) != len(set(aliases)):
250
+ raise ValueError(
251
+ "Each tool must have a unique tool_alias. "
252
+ "Duplicate aliases found in tools list."
253
+ )
254
+ return self
255
+
256
+ @model_validator(mode="after")
257
+ def validate_mcp_server_id_uniqueness(self) -> Self:
258
+ """Ensure mcp_server_id is unique within the mcp_servers list."""
259
+ if self.mcp_servers:
260
+ ids = [m.mcp_server_id for m in self.mcp_servers]
261
+ if len(ids) != len(set(ids)):
262
+ raise ValueError(
263
+ "Duplicate mcp_server_id found in mcp_servers list. "
264
+ "Each MCP server can only be attached once."
265
+ )
266
+ return self
267
+
268
+
269
+ # Response type alias - update returns same structure as create
270
+ UpdateAgentResponse = CreateAgentResponse
271
+
272
+
273
+ # ============================================================================
274
+ # List Agents DTOs
275
+ # ============================================================================
276
+
277
+
278
+ class AgentSummary(BaseModel):
279
+ """Summary of an agent for list responses."""
280
+
281
+ id: UUID
282
+ name: str
283
+ description: str | None
284
+ organization_id: UUID | None
285
+ user_id: UUID | None
286
+ model_count: int
287
+ system_instruction_version_id: UUID | None
288
+ input_schema_id: UUID | None
289
+ output_schema_id: UUID | None
290
+ created_at: datetime
291
+ updated_at: datetime
292
+ usage_count: int | None = Field(
293
+ default=None,
294
+ description="Number of named sessions using this agent. Only populated when sort_by=usage.",
295
+ )
296
+
297
+
298
+ class ListAgentsResponse(BaseModel):
299
+ """Response for listing agents."""
300
+
301
+ agents: list[AgentSummary]
302
+ total: int
303
+
304
+
305
+ # ============================================================================
306
+ # Get Agent DTO
307
+ # ============================================================================
308
+
309
+
310
+ class GetAgentResponse(BaseModel):
311
+ """Response for getting a single agent with full details."""
312
+
313
+ id: UUID
314
+ name: str
315
+ organization_id: UUID | None
316
+ user_id: UUID | None
317
+ base_agent_id: UUID | None
318
+ description: str | None
319
+ system_instruction_version_id: UUID | None
320
+ input_schema_id: UUID | None
321
+ output_schema_id: UUID | None
322
+ default_request_headers: dict[str, str] | None
323
+ generation_params: GenerationParameters | None = None
324
+ created_at: datetime
325
+ updated_at: datetime
326
+ models: list[AgentModelResponse]
327
+ tools: list[AgentToolResponse]
328
+ tags: list[AgentTagResponse]
329
+ mcp_servers: list[AgentMCPServerResponse] = []
330
+
331
+
332
+ # ============================================================================
333
+ # Delete Agent DTOs
334
+ # ============================================================================
335
+
336
+
337
+ class DeletedAgentResponse(BaseModel):
338
+ """Response model for a deleted agent's final state."""
339
+
340
+ id: UUID
341
+ name: str
342
+ organization_id: UUID | None
343
+ user_id: UUID | None
344
+ base_agent_id: UUID | None
345
+ description: str | None
346
+ system_instruction_version_id: UUID | None
347
+ input_schema_id: UUID | None
348
+ output_schema_id: UUID | None
349
+ default_request_headers: dict[str, str] | None
350
+ generation_params: GenerationParameters | None = None
351
+ created_at: datetime
352
+ updated_at: datetime
353
+ models: list[AgentModelResponse]
354
+ tools: list[AgentToolResponse]
355
+ mcp_servers: list[AgentMCPServerResponse] = []
356
+ tags: list[AgentTagResponse]
357
+
358
+
359
+ class DeleteAgentResponse(BaseModel):
360
+ """Response after deleting an agent."""
361
+
362
+ deleted_agent: DeletedAgentResponse
@@ -0,0 +1,124 @@
1
+ """DTOs for API key management operations."""
2
+
3
+ from datetime import datetime
4
+ from uuid import UUID
5
+
6
+ from pydantic import BaseModel, Field, field_validator
7
+
8
+ from alloy_runtime_types.enums.api_key_scope import CANONICAL_SCOPE_ORDER, ApiKeyScope
9
+
10
+
11
+ class CreateApiKeyRequest(BaseModel):
12
+ """Request to create a new API key.
13
+
14
+ Requires explicit non-empty scopes. Duplicates are normalized to unique set
15
+ in canonical order: read, update, delete, use.
16
+ """
17
+
18
+ scopes: list[ApiKeyScope] = Field(
19
+ ...,
20
+ description="Permission scopes for the API key (non-empty, normalized to unique set)",
21
+ min_length=1,
22
+ )
23
+ description: str | None = Field(
24
+ default=None,
25
+ max_length=500,
26
+ description="User-visible label for the key",
27
+ )
28
+ expires_at: datetime | None = Field(
29
+ default=None,
30
+ description="Optional expiration timestamp",
31
+ )
32
+
33
+ @field_validator("scopes")
34
+ @classmethod
35
+ def normalize_scopes(cls, value: list[ApiKeyScope]) -> list[ApiKeyScope]:
36
+ """Normalize scopes to unique set in canonical order."""
37
+ # Remove duplicates while preserving order
38
+ seen: set[ApiKeyScope] = set()
39
+ unique: list[ApiKeyScope] = []
40
+ for scope in value:
41
+ if scope not in seen:
42
+ seen.add(scope)
43
+ unique.append(scope)
44
+
45
+ # Sort by canonical order
46
+ return sorted(unique, key=lambda s: CANONICAL_SCOPE_ORDER.index(s))
47
+
48
+
49
+ class CreateApiKeyResponse(BaseModel):
50
+ """Response after creating an API key.
51
+
52
+ Returns the plaintext key once. Never exposes hashed_key.
53
+ """
54
+
55
+ id: UUID
56
+ key: str # Plaintext, only returned once
57
+ prefix: str
58
+ description: str | None
59
+ scopes: list[ApiKeyScope]
60
+ created_at: datetime
61
+ expires_at: datetime | None = None
62
+
63
+
64
+ class ApiKeyResponse(BaseModel):
65
+ """Common response shape for API key data."""
66
+
67
+ id: UUID
68
+ prefix: str
69
+ description: str | None
70
+ scopes: list[ApiKeyScope]
71
+ created_at: datetime
72
+ expires_at: datetime | None = None
73
+ last_used_at: datetime | None = None
74
+
75
+
76
+ class ListApiKeysResponse(BaseModel):
77
+ """Response listing all API keys for authenticated user."""
78
+
79
+ keys: list[ApiKeyResponse]
80
+ total: int
81
+
82
+
83
+ class UpdateApiKeyRequest(BaseModel):
84
+ """Request to update an existing API key.
85
+
86
+ All fields are optional - only provided fields are updated.
87
+ """
88
+
89
+ description: str | None = Field(
90
+ default=None,
91
+ max_length=500,
92
+ description="User-visible label for the key",
93
+ )
94
+ expires_at: datetime | None = Field(
95
+ default=None,
96
+ description="Expiration timestamp (set to None to remove expiration)",
97
+ )
98
+ scopes: list[ApiKeyScope] | None = Field(
99
+ default=None,
100
+ description="Permission scopes (non-empty if provided, normalized to unique set)",
101
+ )
102
+
103
+ @field_validator("scopes")
104
+ @classmethod
105
+ def normalize_scopes(
106
+ cls, value: list[ApiKeyScope] | None
107
+ ) -> list[ApiKeyScope] | None:
108
+ """Normalize scopes to unique set in canonical order if provided."""
109
+ if value is None:
110
+ return None
111
+
112
+ if len(value) == 0:
113
+ raise ValueError("scopes must be non-empty if provided")
114
+
115
+ # Remove duplicates while preserving order
116
+ seen: set[ApiKeyScope] = set()
117
+ unique: list[ApiKeyScope] = []
118
+ for scope in value:
119
+ if scope not in seen:
120
+ seen.add(scope)
121
+ unique.append(scope)
122
+
123
+ # Sort by canonical order
124
+ return sorted(unique, key=lambda s: CANONICAL_SCOPE_ORDER.index(s))
@@ -0,0 +1,9 @@
1
+ """DTOs for audio transcription endpoints."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class AudioTranscriptResponse(BaseModel):
7
+ """Response containing transcribed text from audio."""
8
+
9
+ text: str = Field(min_length=1, description="Transcribed text from audio")