llama-stack 0.4.4__py3-none-any.whl → 0.5.0__py3-none-any.whl

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 (159) hide show
  1. llama_stack/cli/stack/_list_deps.py +11 -7
  2. llama_stack/cli/stack/run.py +3 -25
  3. llama_stack/core/access_control/datatypes.py +78 -0
  4. llama_stack/core/configure.py +2 -2
  5. llama_stack/{distributions/meta-reference-gpu → core/connectors}/__init__.py +3 -1
  6. llama_stack/core/connectors/connectors.py +162 -0
  7. llama_stack/core/conversations/conversations.py +61 -58
  8. llama_stack/core/datatypes.py +54 -8
  9. llama_stack/core/library_client.py +60 -13
  10. llama_stack/core/prompts/prompts.py +43 -42
  11. llama_stack/core/routers/datasets.py +20 -17
  12. llama_stack/core/routers/eval_scoring.py +143 -53
  13. llama_stack/core/routers/inference.py +20 -9
  14. llama_stack/core/routers/safety.py +30 -42
  15. llama_stack/core/routers/vector_io.py +15 -7
  16. llama_stack/core/routing_tables/models.py +42 -3
  17. llama_stack/core/routing_tables/scoring_functions.py +19 -19
  18. llama_stack/core/routing_tables/shields.py +20 -17
  19. llama_stack/core/routing_tables/vector_stores.py +8 -5
  20. llama_stack/core/server/auth.py +192 -17
  21. llama_stack/core/server/fastapi_router_registry.py +40 -5
  22. llama_stack/core/server/server.py +24 -5
  23. llama_stack/core/stack.py +54 -10
  24. llama_stack/core/storage/datatypes.py +9 -0
  25. llama_stack/core/store/registry.py +1 -1
  26. llama_stack/core/utils/exec.py +2 -2
  27. llama_stack/core/utils/type_inspection.py +16 -2
  28. llama_stack/distributions/dell/config.yaml +4 -1
  29. llama_stack/distributions/dell/run-with-safety.yaml +4 -1
  30. llama_stack/distributions/nvidia/config.yaml +4 -1
  31. llama_stack/distributions/nvidia/run-with-safety.yaml +4 -1
  32. llama_stack/distributions/oci/config.yaml +4 -1
  33. llama_stack/distributions/open-benchmark/config.yaml +9 -1
  34. llama_stack/distributions/postgres-demo/config.yaml +1 -1
  35. llama_stack/distributions/starter/build.yaml +62 -0
  36. llama_stack/distributions/starter/config.yaml +22 -3
  37. llama_stack/distributions/starter/run-with-postgres-store.yaml +22 -3
  38. llama_stack/distributions/starter/starter.py +13 -1
  39. llama_stack/distributions/starter-gpu/build.yaml +62 -0
  40. llama_stack/distributions/starter-gpu/config.yaml +22 -3
  41. llama_stack/distributions/starter-gpu/run-with-postgres-store.yaml +22 -3
  42. llama_stack/distributions/template.py +10 -2
  43. llama_stack/distributions/watsonx/config.yaml +4 -1
  44. llama_stack/log.py +1 -0
  45. llama_stack/providers/inline/agents/meta_reference/__init__.py +1 -0
  46. llama_stack/providers/inline/agents/meta_reference/agents.py +58 -61
  47. llama_stack/providers/inline/agents/meta_reference/responses/openai_responses.py +53 -51
  48. llama_stack/providers/inline/agents/meta_reference/responses/streaming.py +99 -22
  49. llama_stack/providers/inline/agents/meta_reference/responses/types.py +2 -1
  50. llama_stack/providers/inline/agents/meta_reference/responses/utils.py +4 -1
  51. llama_stack/providers/inline/agents/meta_reference/safety.py +2 -2
  52. llama_stack/providers/inline/batches/reference/batches.py +2 -1
  53. llama_stack/providers/inline/eval/meta_reference/eval.py +40 -32
  54. llama_stack/providers/inline/post_training/huggingface/post_training.py +33 -38
  55. llama_stack/providers/inline/post_training/huggingface/utils.py +2 -5
  56. llama_stack/providers/inline/post_training/torchtune/common/utils.py +5 -9
  57. llama_stack/providers/inline/post_training/torchtune/post_training.py +28 -33
  58. llama_stack/providers/inline/post_training/torchtune/recipes/lora_finetuning_single_device.py +2 -4
  59. llama_stack/providers/inline/safety/code_scanner/code_scanner.py +12 -15
  60. llama_stack/providers/inline/safety/llama_guard/llama_guard.py +20 -24
  61. llama_stack/providers/inline/safety/prompt_guard/prompt_guard.py +11 -17
  62. llama_stack/providers/inline/scoring/basic/scoring.py +13 -17
  63. llama_stack/providers/inline/scoring/braintrust/braintrust.py +15 -15
  64. llama_stack/providers/inline/scoring/llm_as_judge/scoring.py +13 -17
  65. llama_stack/providers/inline/vector_io/sqlite_vec/sqlite_vec.py +1 -1
  66. llama_stack/providers/registry/agents.py +1 -0
  67. llama_stack/providers/registry/inference.py +1 -9
  68. llama_stack/providers/registry/vector_io.py +136 -16
  69. llama_stack/providers/remote/eval/nvidia/eval.py +22 -21
  70. llama_stack/providers/remote/files/s3/config.py +5 -3
  71. llama_stack/providers/remote/files/s3/files.py +2 -2
  72. llama_stack/providers/remote/inference/gemini/gemini.py +4 -0
  73. llama_stack/providers/remote/inference/openai/openai.py +2 -0
  74. llama_stack/providers/remote/inference/together/together.py +4 -0
  75. llama_stack/providers/remote/inference/vertexai/config.py +3 -3
  76. llama_stack/providers/remote/inference/vertexai/vertexai.py +5 -2
  77. llama_stack/providers/remote/inference/vllm/config.py +37 -18
  78. llama_stack/providers/remote/inference/vllm/vllm.py +0 -3
  79. llama_stack/providers/remote/inference/watsonx/watsonx.py +4 -0
  80. llama_stack/providers/remote/post_training/nvidia/models.py +3 -11
  81. llama_stack/providers/remote/post_training/nvidia/post_training.py +31 -33
  82. llama_stack/providers/remote/safety/bedrock/bedrock.py +10 -27
  83. llama_stack/providers/remote/safety/nvidia/nvidia.py +9 -25
  84. llama_stack/providers/remote/safety/sambanova/sambanova.py +13 -11
  85. llama_stack/providers/remote/vector_io/elasticsearch/__init__.py +17 -0
  86. llama_stack/providers/remote/vector_io/elasticsearch/config.py +32 -0
  87. llama_stack/providers/remote/vector_io/elasticsearch/elasticsearch.py +463 -0
  88. llama_stack/providers/remote/vector_io/oci/__init__.py +22 -0
  89. llama_stack/providers/remote/vector_io/oci/config.py +41 -0
  90. llama_stack/providers/remote/vector_io/oci/oci26ai.py +595 -0
  91. llama_stack/providers/remote/vector_io/pgvector/config.py +69 -2
  92. llama_stack/providers/remote/vector_io/pgvector/pgvector.py +255 -6
  93. llama_stack/providers/remote/vector_io/qdrant/qdrant.py +62 -38
  94. llama_stack/providers/utils/bedrock/client.py +3 -3
  95. llama_stack/providers/utils/bedrock/config.py +7 -7
  96. llama_stack/providers/utils/inference/__init__.py +0 -25
  97. llama_stack/providers/utils/inference/embedding_mixin.py +4 -0
  98. llama_stack/providers/utils/inference/http_client.py +239 -0
  99. llama_stack/providers/utils/inference/litellm_openai_mixin.py +6 -0
  100. llama_stack/providers/utils/inference/model_registry.py +148 -2
  101. llama_stack/providers/utils/inference/openai_compat.py +1 -158
  102. llama_stack/providers/utils/inference/openai_mixin.py +42 -2
  103. llama_stack/providers/utils/inference/prompt_adapter.py +0 -209
  104. llama_stack/providers/utils/memory/openai_vector_store_mixin.py +92 -5
  105. llama_stack/providers/utils/memory/vector_store.py +46 -19
  106. llama_stack/providers/utils/responses/responses_store.py +7 -7
  107. llama_stack/providers/utils/safety.py +114 -0
  108. llama_stack/providers/utils/tools/mcp.py +44 -3
  109. llama_stack/testing/api_recorder.py +9 -3
  110. {llama_stack-0.4.4.dist-info → llama_stack-0.5.0.dist-info}/METADATA +14 -2
  111. {llama_stack-0.4.4.dist-info → llama_stack-0.5.0.dist-info}/RECORD +115 -148
  112. llama_stack/distributions/meta-reference-gpu/config.yaml +0 -140
  113. llama_stack/distributions/meta-reference-gpu/doc_template.md +0 -119
  114. llama_stack/distributions/meta-reference-gpu/meta_reference.py +0 -163
  115. llama_stack/distributions/meta-reference-gpu/run-with-safety.yaml +0 -155
  116. llama_stack/models/llama/hadamard_utils.py +0 -88
  117. llama_stack/models/llama/llama3/args.py +0 -74
  118. llama_stack/models/llama/llama3/dog.jpg +0 -0
  119. llama_stack/models/llama/llama3/generation.py +0 -378
  120. llama_stack/models/llama/llama3/model.py +0 -304
  121. llama_stack/models/llama/llama3/multimodal/__init__.py +0 -12
  122. llama_stack/models/llama/llama3/multimodal/encoder_utils.py +0 -180
  123. llama_stack/models/llama/llama3/multimodal/image_transform.py +0 -409
  124. llama_stack/models/llama/llama3/multimodal/model.py +0 -1430
  125. llama_stack/models/llama/llama3/multimodal/utils.py +0 -26
  126. llama_stack/models/llama/llama3/pasta.jpeg +0 -0
  127. llama_stack/models/llama/llama3/quantization/__init__.py +0 -5
  128. llama_stack/models/llama/llama3/quantization/loader.py +0 -316
  129. llama_stack/models/llama/llama3_1/__init__.py +0 -12
  130. llama_stack/models/llama/llama3_1/prompt_format.md +0 -358
  131. llama_stack/models/llama/llama3_1/prompts.py +0 -258
  132. llama_stack/models/llama/llama3_2/__init__.py +0 -5
  133. llama_stack/models/llama/llama3_2/prompts_text.py +0 -229
  134. llama_stack/models/llama/llama3_2/prompts_vision.py +0 -126
  135. llama_stack/models/llama/llama3_2/text_prompt_format.md +0 -286
  136. llama_stack/models/llama/llama3_2/vision_prompt_format.md +0 -141
  137. llama_stack/models/llama/llama3_3/__init__.py +0 -5
  138. llama_stack/models/llama/llama3_3/prompts.py +0 -259
  139. llama_stack/models/llama/llama4/args.py +0 -107
  140. llama_stack/models/llama/llama4/ffn.py +0 -58
  141. llama_stack/models/llama/llama4/moe.py +0 -214
  142. llama_stack/models/llama/llama4/preprocess.py +0 -435
  143. llama_stack/models/llama/llama4/quantization/__init__.py +0 -5
  144. llama_stack/models/llama/llama4/quantization/loader.py +0 -226
  145. llama_stack/models/llama/llama4/vision/__init__.py +0 -5
  146. llama_stack/models/llama/llama4/vision/embedding.py +0 -210
  147. llama_stack/models/llama/llama4/vision/encoder.py +0 -412
  148. llama_stack/models/llama/quantize_impls.py +0 -316
  149. llama_stack/providers/inline/inference/meta_reference/__init__.py +0 -20
  150. llama_stack/providers/inline/inference/meta_reference/common.py +0 -24
  151. llama_stack/providers/inline/inference/meta_reference/config.py +0 -68
  152. llama_stack/providers/inline/inference/meta_reference/generators.py +0 -201
  153. llama_stack/providers/inline/inference/meta_reference/inference.py +0 -542
  154. llama_stack/providers/inline/inference/meta_reference/model_parallel.py +0 -77
  155. llama_stack/providers/inline/inference/meta_reference/parallel_utils.py +0 -353
  156. {llama_stack-0.4.4.dist-info → llama_stack-0.5.0.dist-info}/WHEEL +0 -0
  157. {llama_stack-0.4.4.dist-info → llama_stack-0.5.0.dist-info}/entry_points.txt +0 -0
  158. {llama_stack-0.4.4.dist-info → llama_stack-0.5.0.dist-info}/licenses/LICENSE +0 -0
  159. {llama_stack-0.4.4.dist-info → llama_stack-0.5.0.dist-info}/top_level.txt +0 -0
@@ -47,16 +47,20 @@ def format_output_deps_only(
47
47
  uv_str = ""
48
48
  if uv:
49
49
  uv_str = "uv pip install "
50
-
51
- # Quote deps with commas
52
- quoted_normal_deps = [quote_if_needed(dep) for dep in normal_deps]
53
- lines.append(f"{uv_str}{' '.join(quoted_normal_deps)}")
50
+ # Only quote when emitting a shell command. In deps-only mode, keep raw
51
+ # specs so they can be safely consumed via command substitution.
52
+ formatted_normal_deps = [quote_if_needed(dep) for dep in normal_deps]
53
+ else:
54
+ formatted_normal_deps = normal_deps
55
+ lines.append(f"{uv_str}{' '.join(formatted_normal_deps)}")
54
56
 
55
57
  for special_dep in special_deps:
56
- lines.append(f"{uv_str}{quote_special_dep(special_dep)}")
58
+ formatted = quote_special_dep(special_dep) if uv else special_dep
59
+ lines.append(f"{uv_str}{formatted}")
57
60
 
58
61
  for external_dep in external_deps:
59
- lines.append(f"{uv_str}{quote_special_dep(external_dep)}")
62
+ formatted = quote_special_dep(external_dep) if uv else external_dep
63
+ lines.append(f"{uv_str}{formatted}")
60
64
 
61
65
  return "\n".join(lines)
62
66
 
@@ -119,7 +123,7 @@ def run_stack_list_deps_command(args: argparse.Namespace) -> None:
119
123
  file=sys.stderr,
120
124
  )
121
125
  sys.exit(1)
122
- config = StackConfig(providers=provider_list, image_name="providers-run")
126
+ config = StackConfig(providers=provider_list, distro_name="providers-run")
123
127
 
124
128
  normal_deps, special_deps, external_provider_dependencies = get_provider_dependencies(config)
125
129
  normal_deps += SERVER_DEPENDENCIES
@@ -15,11 +15,10 @@ import uvicorn
15
15
  import yaml
16
16
  from termcolor import cprint
17
17
 
18
- from llama_stack.cli.stack.utils import ImageType
19
18
  from llama_stack.cli.subcommand import Subcommand
20
19
  from llama_stack.core.datatypes import Api, Provider, StackConfig
21
20
  from llama_stack.core.distribution import get_provider_registry
22
- from llama_stack.core.stack import cast_image_name_to_string, replace_env_vars
21
+ from llama_stack.core.stack import cast_distro_name_to_string, replace_env_vars
23
22
  from llama_stack.core.storage.datatypes import (
24
23
  InferenceStoreReference,
25
24
  KVStoreReference,
@@ -65,18 +64,6 @@ class StackRun(Subcommand):
65
64
  help="Port to run the server on. It can also be passed via the env var LLAMA_STACK_PORT.",
66
65
  default=int(os.getenv("LLAMA_STACK_PORT", 8321)),
67
66
  )
68
- self.parser.add_argument(
69
- "--image-name",
70
- type=str,
71
- default=None,
72
- help="[DEPRECATED] This flag is no longer supported. Please activate your virtual environment before running.",
73
- )
74
- self.parser.add_argument(
75
- "--image-type",
76
- type=str,
77
- help="[DEPRECATED] This flag is no longer supported. Please activate your virtual environment before running.",
78
- choices=[e.value for e in ImageType if e.value != ImageType.CONTAINER.value],
79
- )
80
67
  self.parser.add_argument(
81
68
  "--enable-ui",
82
69
  action="store_true",
@@ -94,15 +81,6 @@ class StackRun(Subcommand):
94
81
 
95
82
  from llama_stack.core.configure import parse_and_maybe_upgrade_config
96
83
 
97
- if args.image_type or args.image_name:
98
- self.parser.error(
99
- "The --image-type and --image-name flags are no longer supported.\n\n"
100
- "Please activate your virtual environment manually before running `llama stack run`.\n\n"
101
- "For example:\n"
102
- " source /path/to/venv/bin/activate\n"
103
- " llama stack run <config>\n"
104
- )
105
-
106
84
  if args.enable_ui:
107
85
  self._start_ui_development_server(args.port)
108
86
 
@@ -194,7 +172,7 @@ class StackRun(Subcommand):
194
172
  logger_config = LoggingConfig(**cfg)
195
173
  else:
196
174
  logger_config = None
197
- config = StackConfig(**cast_image_name_to_string(replace_env_vars(config_contents)))
175
+ config = StackConfig(**cast_distro_name_to_string(replace_env_vars(config_contents)))
198
176
 
199
177
  port = args.port or config.server.port
200
178
  host = config.server.host or ["::", "0.0.0.0"]
@@ -322,7 +300,7 @@ class StackRun(Subcommand):
322
300
  )
323
301
 
324
302
  return StackConfig(
325
- image_name="providers-run",
303
+ distro_name="providers-run",
326
304
  apis=apis,
327
305
  providers=providers,
328
306
  storage=storage,
@@ -25,6 +25,20 @@ class Scope(BaseModel):
25
25
  resource: str | None = None
26
26
 
27
27
 
28
+ class RouteScope(BaseModel):
29
+ """Scope for route-level access control.
30
+
31
+ Defines which API routes can be accessed. The paths field
32
+ accepts single paths, lists of paths, or wildcards:
33
+ - Exact: "/v1/chat/completions"
34
+ - List: ["/v1/files*", "/v1/models*"]
35
+ - Prefix wildcard: "/v1/files*" matches "/v1/files" and all paths starting with "/v1/files"
36
+ - Full wildcard: "*"
37
+ """
38
+
39
+ paths: str | list[str]
40
+
41
+
28
42
  def _mutually_exclusive(obj, a: str, b: str):
29
43
  if getattr(obj, a) and getattr(obj, b):
30
44
  raise ValueError(f"{a} and {b} are mutually exclusive")
@@ -105,3 +119,67 @@ class AccessRule(BaseModel):
105
119
  elif self.unless:
106
120
  parse_conditions([self.unless])
107
121
  return self
122
+
123
+
124
+ class RouteAccessRule(BaseModel):
125
+ """Route-level access rule for controlling API route access.
126
+
127
+ This rule defines which API routes users can access based on their
128
+ attributes (roles, teams, etc). Rules are evaluated before resource-level
129
+ access control.
130
+
131
+ A rule defines either permit or forbid access to specific routes. The routes
132
+ are specified using the 'paths' field which can be:
133
+ - A single exact path: "/v1/chat/completions"
134
+ - A list of paths: ["/v1/files*", "/v1/models*"]
135
+ - A wildcard prefix: "/v1/files*" matches /v1/files and all paths starting with /v1/files
136
+ - Full wildcard: "*" matches all routes
137
+
138
+ Path normalization: Trailing slashes are automatically removed (e.g., /v1/files/ becomes /v1/files).
139
+
140
+ A rule may also specify a condition using 'when' or 'unless', with the same
141
+ constraints as resource-level rules:
142
+ - 'user with <attr-value> in <attr-name>'
143
+ - 'user with <attr-value> not in <attr-name>'
144
+
145
+ If no route_policy is configured, all routes are allowed.
146
+ If route_policy is configured, rules are tested in order to find a match.
147
+
148
+ Examples in yaml:
149
+
150
+ - permit:
151
+ paths: "/v1/chat/completions"
152
+ when: user with developer in roles
153
+ description: developers can access chat completions
154
+
155
+ - permit:
156
+ paths: ["/v1/files*", "/v1/models*"]
157
+ when: user with user in roles
158
+ description: users can access files and models routes
159
+
160
+ - permit:
161
+ paths: "*"
162
+ when: user with admin in roles
163
+ description: admins have access to all routes
164
+ """
165
+
166
+ permit: RouteScope | None = None
167
+ forbid: RouteScope | None = None
168
+ when: str | list[str] | None = None
169
+ unless: str | list[str] | None = None
170
+ description: str | None = None
171
+
172
+ @model_validator(mode="after")
173
+ def validate_rule_format(self) -> Self:
174
+ _require_one_of(self, "permit", "forbid")
175
+ _mutually_exclusive(self, "permit", "forbid")
176
+ _mutually_exclusive(self, "when", "unless")
177
+ if isinstance(self.when, list):
178
+ parse_conditions(self.when)
179
+ elif self.when:
180
+ parse_conditions([self.when])
181
+ if isinstance(self.unless, list):
182
+ parse_conditions(self.unless)
183
+ elif self.unless:
184
+ parse_conditions([self.unless])
185
+ return self
@@ -16,7 +16,7 @@ from llama_stack.core.distribution import (
16
16
  builtin_automatically_routed_apis,
17
17
  get_provider_registry,
18
18
  )
19
- from llama_stack.core.stack import cast_image_name_to_string, replace_env_vars
19
+ from llama_stack.core.stack import cast_distro_name_to_string, replace_env_vars
20
20
  from llama_stack.core.utils.dynamic import instantiate_class_type
21
21
  from llama_stack.core.utils.prompt_for_config import prompt_for_config
22
22
  from llama_stack.log import get_logger
@@ -200,4 +200,4 @@ def parse_and_maybe_upgrade_config(config_dict: dict[str, Any]) -> StackConfig:
200
200
  config_dict["version"] = LLAMA_STACK_RUN_CONFIG_VERSION
201
201
 
202
202
  processed_config_dict = replace_env_vars(config_dict)
203
- return StackConfig(**cast_image_name_to_string(processed_config_dict))
203
+ return StackConfig(**cast_distro_name_to_string(processed_config_dict))
@@ -4,4 +4,6 @@
4
4
  # This source code is licensed under the terms described in the LICENSE file in
5
5
  # the root directory of this source tree.
6
6
 
7
- from .meta_reference import get_distribution_template # noqa: F401
7
+ from llama_stack.core.connectors.connectors import ConnectorServiceConfig, ConnectorServiceImpl
8
+
9
+ __all__ = ["ConnectorServiceConfig", "ConnectorServiceImpl"]
@@ -0,0 +1,162 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the terms described in the LICENSE file in
5
+ # the root directory of this source tree.
6
+
7
+ import json
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+ from llama_stack.core.datatypes import StackConfig
12
+ from llama_stack.core.storage.kvstore import KVStore, kvstore_impl
13
+ from llama_stack.log import get_logger
14
+ from llama_stack.providers.utils.tools.mcp import get_mcp_server_info, list_mcp_tools
15
+ from llama_stack_api import (
16
+ Connector,
17
+ ConnectorNotFoundError,
18
+ Connectors,
19
+ ConnectorToolNotFoundError,
20
+ ConnectorType,
21
+ GetConnectorRequest,
22
+ GetConnectorToolRequest,
23
+ ListConnectorsResponse,
24
+ ListConnectorToolsRequest,
25
+ ListToolsResponse,
26
+ ToolDef,
27
+ )
28
+
29
+ logger = get_logger(name=__name__, category="connectors")
30
+
31
+
32
+ class ConnectorServiceConfig(BaseModel):
33
+ """Configuration for the built-in connector service."""
34
+
35
+ config: StackConfig = Field(..., description="Stack run configuration for resolving persistence")
36
+
37
+
38
+ async def get_provider_impl(config: ConnectorServiceConfig):
39
+ """Get the connector service implementation."""
40
+ impl = ConnectorServiceImpl(config)
41
+ return impl
42
+
43
+
44
+ KEY_PREFIX = "connectors:v1:"
45
+
46
+
47
+ class ConnectorServiceImpl(Connectors):
48
+ """Built-in connector service implementation."""
49
+
50
+ def __init__(self, config: ConnectorServiceConfig):
51
+ self.config = config
52
+ self.kvstore: KVStore
53
+
54
+ def _get_key(self, connector_id: str) -> str:
55
+ """Get the KVStore key for a connector."""
56
+ return f"{KEY_PREFIX}{connector_id}"
57
+
58
+ async def initialize(self):
59
+ """Initialize the connector service."""
60
+
61
+ # Use connectors store reference from run config
62
+ connectors_ref = self.config.config.storage.stores.connectors
63
+ if not connectors_ref:
64
+ raise ValueError("storage.stores.connectors must be configured in config")
65
+ self.kvstore = await kvstore_impl(connectors_ref)
66
+
67
+ async def register_connector(
68
+ self,
69
+ connector_id: str,
70
+ connector_type: ConnectorType,
71
+ url: str,
72
+ server_label: str | None = None,
73
+ server_name: str | None = None,
74
+ server_description: str | None = None,
75
+ ) -> Connector:
76
+ """Register a new connector"""
77
+
78
+ connector = Connector(
79
+ connector_id=connector_id,
80
+ connector_type=connector_type,
81
+ url=url,
82
+ server_label=server_label,
83
+ server_name=server_name,
84
+ server_description=server_description,
85
+ )
86
+
87
+ key = self._get_key(connector_id)
88
+ existing_connector_json = await self.kvstore.get(key)
89
+
90
+ if existing_connector_json:
91
+ existing_connector = Connector.model_validate_json(existing_connector_json)
92
+
93
+ if connector == existing_connector:
94
+ logger.info(
95
+ "Connector %s already exists; skipping registration",
96
+ connector_id,
97
+ )
98
+ return existing_connector
99
+
100
+ await self.kvstore.set(key, json.dumps(connector.model_dump()))
101
+
102
+ return connector
103
+
104
+ async def unregister_connector(self, connector_id: str):
105
+ """Unregister a connector."""
106
+ key = self._get_key(connector_id)
107
+ if not await self.kvstore.get(key):
108
+ return
109
+ await self.kvstore.delete(key)
110
+
111
+ async def get_connector(
112
+ self,
113
+ request: GetConnectorRequest,
114
+ authorization: str | None = None,
115
+ ) -> Connector:
116
+ """Get a connector by its ID."""
117
+
118
+ connector_json = await self.kvstore.get(self._get_key(request.connector_id))
119
+ if not connector_json:
120
+ raise ConnectorNotFoundError(request.connector_id)
121
+ connector = Connector.model_validate_json(connector_json)
122
+
123
+ server_info = await get_mcp_server_info(connector.url, authorization=authorization)
124
+ connector.server_name = server_info.name
125
+ connector.server_description = server_info.description
126
+ connector.server_version = server_info.version
127
+ return connector
128
+
129
+ async def list_connectors(self) -> ListConnectorsResponse:
130
+ """List all connectors."""
131
+ connectors = []
132
+ for key in await self.kvstore.keys_in_range(start_key=KEY_PREFIX, end_key=KEY_PREFIX + "\uffff"):
133
+ connector_json = await self.kvstore.get(key)
134
+ if not connector_json:
135
+ continue
136
+ connector = Connector.model_validate_json(connector_json)
137
+ connectors.append(connector)
138
+ return ListConnectorsResponse(data=connectors)
139
+
140
+ async def get_connector_tool(self, request: GetConnectorToolRequest, authorization: str | None = None) -> ToolDef:
141
+ """Get a tool from a connector."""
142
+ connector_tools = await self.list_connector_tools(
143
+ ListConnectorToolsRequest(connector_id=request.connector_id), authorization=authorization
144
+ )
145
+ for tool in connector_tools.data:
146
+ if tool.name == request.tool_name:
147
+ return tool
148
+ raise ConnectorToolNotFoundError(request.connector_id, request.tool_name)
149
+
150
+ async def list_connector_tools(
151
+ self, request: ListConnectorToolsRequest, authorization: str | None = None
152
+ ) -> ListToolsResponse:
153
+ """List tools from a connector."""
154
+ connector = await self.get_connector(
155
+ GetConnectorRequest(connector_id=request.connector_id), authorization=authorization
156
+ )
157
+ tools = await list_mcp_tools(endpoint=connector.url, authorization=authorization)
158
+ return ListToolsResponse(data=tools.data)
159
+
160
+ async def shutdown(self):
161
+ """Shutdown the connector service."""
162
+ await self.kvstore.close()
@@ -6,7 +6,7 @@
6
6
 
7
7
  import secrets
8
8
  import time
9
- from typing import Any, Literal
9
+ from typing import Any
10
10
 
11
11
  from pydantic import BaseModel, TypeAdapter
12
12
 
@@ -14,15 +14,21 @@ from llama_stack.core.datatypes import AccessRule, StackConfig
14
14
  from llama_stack.core.storage.sqlstore.authorized_sqlstore import AuthorizedSqlStore
15
15
  from llama_stack.core.storage.sqlstore.sqlstore import sqlstore_impl
16
16
  from llama_stack.log import get_logger
17
- from llama_stack_api import (
17
+ from llama_stack_api.conversations import (
18
+ AddItemsRequest,
18
19
  Conversation,
19
20
  ConversationDeletedResource,
20
21
  ConversationItem,
21
22
  ConversationItemDeletedResource,
22
- ConversationItemInclude,
23
23
  ConversationItemList,
24
24
  Conversations,
25
- Metadata,
25
+ CreateConversationRequest,
26
+ DeleteConversationRequest,
27
+ DeleteItemRequest,
28
+ GetConversationRequest,
29
+ ListItemsRequest,
30
+ RetrieveItemRequest,
31
+ UpdateConversationRequest,
26
32
  )
27
33
  from llama_stack_api.internal.sqlstore import ColumnDefinition, ColumnType
28
34
 
@@ -85,9 +91,7 @@ class ConversationServiceImpl(Conversations):
85
91
  },
86
92
  )
87
93
 
88
- async def create_conversation(
89
- self, items: list[ConversationItem] | None = None, metadata: Metadata | None = None
90
- ) -> Conversation:
94
+ async def create_conversation(self, request: CreateConversationRequest) -> Conversation:
91
95
  """Create a conversation."""
92
96
  random_bytes = secrets.token_bytes(24)
93
97
  conversation_id = f"conv_{random_bytes.hex()}"
@@ -97,7 +101,7 @@ class ConversationServiceImpl(Conversations):
97
101
  "id": conversation_id,
98
102
  "created_at": created_at,
99
103
  "items": [],
100
- "metadata": metadata,
104
+ "metadata": request.metadata,
101
105
  }
102
106
 
103
107
  await self.sql_store.insert(
@@ -105,9 +109,9 @@ class ConversationServiceImpl(Conversations):
105
109
  data=record_data,
106
110
  )
107
111
 
108
- if items:
112
+ if request.items:
109
113
  item_records = []
110
- for item in items:
114
+ for item in request.items:
111
115
  item_dict = item.model_dump()
112
116
  item_id = self._get_or_generate_item_id(item, item_dict)
113
117
 
@@ -125,38 +129,38 @@ class ConversationServiceImpl(Conversations):
125
129
  conversation = Conversation(
126
130
  id=conversation_id,
127
131
  created_at=created_at,
128
- metadata=metadata,
132
+ metadata=request.metadata,
129
133
  object="conversation",
130
134
  )
131
135
 
132
136
  logger.debug(f"Created conversation {conversation_id}")
133
137
  return conversation
134
138
 
135
- async def get_conversation(self, conversation_id: str) -> Conversation:
139
+ async def get_conversation(self, request: GetConversationRequest) -> Conversation:
136
140
  """Get a conversation with the given ID."""
137
- record = await self.sql_store.fetch_one(table="openai_conversations", where={"id": conversation_id})
141
+ record = await self.sql_store.fetch_one(table="openai_conversations", where={"id": request.conversation_id})
138
142
 
139
143
  if record is None:
140
- raise ValueError(f"Conversation {conversation_id} not found")
144
+ raise ValueError(f"Conversation {request.conversation_id} not found")
141
145
 
142
146
  return Conversation(
143
147
  id=record["id"], created_at=record["created_at"], metadata=record.get("metadata"), object="conversation"
144
148
  )
145
149
 
146
- async def update_conversation(self, conversation_id: str, metadata: Metadata) -> Conversation:
150
+ async def update_conversation(self, conversation_id: str, request: UpdateConversationRequest) -> Conversation:
147
151
  """Update a conversation's metadata with the given ID"""
148
152
  await self.sql_store.update(
149
- table="openai_conversations", data={"metadata": metadata}, where={"id": conversation_id}
153
+ table="openai_conversations", data={"metadata": request.metadata}, where={"id": conversation_id}
150
154
  )
151
155
 
152
- return await self.get_conversation(conversation_id)
156
+ return await self.get_conversation(GetConversationRequest(conversation_id=conversation_id))
153
157
 
154
- async def openai_delete_conversation(self, conversation_id: str) -> ConversationDeletedResource:
158
+ async def openai_delete_conversation(self, request: DeleteConversationRequest) -> ConversationDeletedResource:
155
159
  """Delete a conversation with the given ID."""
156
- await self.sql_store.delete(table="openai_conversations", where={"id": conversation_id})
160
+ await self.sql_store.delete(table="openai_conversations", where={"id": request.conversation_id})
157
161
 
158
- logger.debug(f"Deleted conversation {conversation_id}")
159
- return ConversationDeletedResource(id=conversation_id)
162
+ logger.debug(f"Deleted conversation {request.conversation_id}")
163
+ return ConversationDeletedResource(id=request.conversation_id)
160
164
 
161
165
  def _validate_conversation_id(self, conversation_id: str) -> None:
162
166
  """Validate conversation ID format."""
@@ -180,16 +184,16 @@ class ConversationServiceImpl(Conversations):
180
184
  async def _get_validated_conversation(self, conversation_id: str) -> Conversation:
181
185
  """Validate conversation ID and return the conversation if it exists."""
182
186
  self._validate_conversation_id(conversation_id)
183
- return await self.get_conversation(conversation_id)
187
+ return await self.get_conversation(GetConversationRequest(conversation_id=conversation_id))
184
188
 
185
- async def add_items(self, conversation_id: str, items: list[ConversationItem]) -> ConversationItemList:
189
+ async def add_items(self, conversation_id: str, request: AddItemsRequest) -> ConversationItemList:
186
190
  """Create (add) items to a conversation."""
187
191
  await self._get_validated_conversation(conversation_id)
188
192
 
189
193
  created_items = []
190
194
  base_time = int(time.time())
191
195
 
192
- for i, item in enumerate(items):
196
+ for i, item in enumerate(request.items):
193
197
  item_dict = item.model_dump()
194
198
  item_id = self._get_or_generate_item_id(item, item_dict)
195
199
 
@@ -224,48 +228,47 @@ class ConversationServiceImpl(Conversations):
224
228
  has_more=False,
225
229
  )
226
230
 
227
- async def retrieve(self, conversation_id: str, item_id: str) -> ConversationItem:
231
+ async def retrieve(self, request: RetrieveItemRequest) -> ConversationItem:
228
232
  """Retrieve a conversation item."""
229
- if not conversation_id:
230
- raise ValueError(f"Expected a non-empty value for `conversation_id` but received {conversation_id!r}")
231
- if not item_id:
232
- raise ValueError(f"Expected a non-empty value for `item_id` but received {item_id!r}")
233
+ if not request.conversation_id:
234
+ raise ValueError(
235
+ f"Expected a non-empty value for `conversation_id` but received {request.conversation_id!r}"
236
+ )
237
+ if not request.item_id:
238
+ raise ValueError(f"Expected a non-empty value for `item_id` but received {request.item_id!r}")
233
239
 
234
240
  # Get item from conversation_items table
235
241
  record = await self.sql_store.fetch_one(
236
- table="conversation_items", where={"id": item_id, "conversation_id": conversation_id}
242
+ table="conversation_items", where={"id": request.item_id, "conversation_id": request.conversation_id}
237
243
  )
238
244
 
239
245
  if record is None:
240
- raise ValueError(f"Item {item_id} not found in conversation {conversation_id}")
246
+ raise ValueError(f"Item {request.item_id} not found in conversation {request.conversation_id}")
241
247
 
242
248
  adapter: TypeAdapter[ConversationItem] = TypeAdapter(ConversationItem)
243
249
  return adapter.validate_python(record["item_data"])
244
250
 
245
- async def list_items(
246
- self,
247
- conversation_id: str,
248
- after: str | None = None,
249
- include: list[ConversationItemInclude] | None = None,
250
- limit: int | None = None,
251
- order: Literal["asc", "desc"] | None = None,
252
- ) -> ConversationItemList:
251
+ async def list_items(self, request: ListItemsRequest) -> ConversationItemList:
253
252
  """List items in the conversation."""
254
- if not conversation_id:
255
- raise ValueError(f"Expected a non-empty value for `conversation_id` but received {conversation_id!r}")
253
+ if not request.conversation_id:
254
+ raise ValueError(
255
+ f"Expected a non-empty value for `conversation_id` but received {request.conversation_id!r}"
256
+ )
256
257
 
257
258
  # check if conversation exists
258
- await self.get_conversation(conversation_id)
259
+ await self.get_conversation(GetConversationRequest(conversation_id=request.conversation_id))
259
260
 
260
- result = await self.sql_store.fetch_all(table="conversation_items", where={"conversation_id": conversation_id})
261
+ result = await self.sql_store.fetch_all(
262
+ table="conversation_items", where={"conversation_id": request.conversation_id}
263
+ )
261
264
  records = result.data
262
265
 
263
- if order is not None and order == "asc":
266
+ if request.order is not None and request.order == "asc":
264
267
  records.sort(key=lambda x: x["created_at"])
265
268
  else:
266
269
  records.sort(key=lambda x: x["created_at"], reverse=True)
267
270
 
268
- actual_limit = limit or 20
271
+ actual_limit = request.limit or 20
269
272
 
270
273
  records = records[:actual_limit]
271
274
  items = [record["item_data"] for record in records]
@@ -283,30 +286,30 @@ class ConversationServiceImpl(Conversations):
283
286
  has_more=False,
284
287
  )
285
288
 
286
- async def openai_delete_conversation_item(
287
- self, conversation_id: str, item_id: str
288
- ) -> ConversationItemDeletedResource:
289
+ async def openai_delete_conversation_item(self, request: DeleteItemRequest) -> ConversationItemDeletedResource:
289
290
  """Delete a conversation item."""
290
- if not conversation_id:
291
- raise ValueError(f"Expected a non-empty value for `conversation_id` but received {conversation_id!r}")
292
- if not item_id:
293
- raise ValueError(f"Expected a non-empty value for `item_id` but received {item_id!r}")
291
+ if not request.conversation_id:
292
+ raise ValueError(
293
+ f"Expected a non-empty value for `conversation_id` but received {request.conversation_id!r}"
294
+ )
295
+ if not request.item_id:
296
+ raise ValueError(f"Expected a non-empty value for `item_id` but received {request.item_id!r}")
294
297
 
295
- _ = await self._get_validated_conversation(conversation_id)
298
+ _ = await self._get_validated_conversation(request.conversation_id)
296
299
 
297
300
  record = await self.sql_store.fetch_one(
298
- table="conversation_items", where={"id": item_id, "conversation_id": conversation_id}
301
+ table="conversation_items", where={"id": request.item_id, "conversation_id": request.conversation_id}
299
302
  )
300
303
 
301
304
  if record is None:
302
- raise ValueError(f"Item {item_id} not found in conversation {conversation_id}")
305
+ raise ValueError(f"Item {request.item_id} not found in conversation {request.conversation_id}")
303
306
 
304
307
  await self.sql_store.delete(
305
- table="conversation_items", where={"id": item_id, "conversation_id": conversation_id}
308
+ table="conversation_items", where={"id": request.item_id, "conversation_id": request.conversation_id}
306
309
  )
307
310
 
308
- logger.debug(f"Deleted item {item_id} from conversation {conversation_id}")
309
- return ConversationItemDeletedResource(id=item_id)
311
+ logger.debug(f"Deleted item {request.item_id} from conversation {request.conversation_id}")
312
+ return ConversationItemDeletedResource(id=request.item_id)
310
313
 
311
314
  async def shutdown(self) -> None:
312
315
  pass