From 13b6f3df6568682a12870f5b5ce6f048853f6445 Mon Sep 17 00:00:00 2001 From: Raghotham Murthy Date: Mon, 27 Oct 2025 10:52:20 -0700 Subject: [PATCH] updates --- client-sdks/stainless/openapi.yml | 209 +++++++++----- docs/static/deprecated-llama-stack-spec.html | 148 ++++++++++ docs/static/deprecated-llama-stack-spec.yaml | 105 +++++++ docs/static/llama-stack-spec.html | 280 ++++++++++++------- docs/static/llama-stack-spec.yaml | 209 +++++++++----- docs/static/stainless-llama-stack-spec.html | 280 ++++++++++++------- docs/static/stainless-llama-stack-spec.yaml | 209 +++++++++----- llama_stack/apis/providers/providers.py | 63 +++-- llama_stack/core/providers.py | 201 +++++++------ llama_stack/core/stack.py | 71 ++--- llama_stack/distributions/ci-tests/run.yaml | 2 +- tests/unit/core/test_dynamic_providers.py | 66 +++-- 12 files changed, 1238 insertions(+), 605 deletions(-) diff --git a/client-sdks/stainless/openapi.yml b/client-sdks/stainless/openapi.yml index 2953055a5..e05d6eba1 100644 --- a/client-sdks/stainless/openapi.yml +++ b/client-sdks/stainless/openapi.yml @@ -15,7 +15,7 @@ info: servers: - url: http://any-hosted-llama-stack.com paths: - /v1/admin/providers: + /v1/admin/providers/{api}: post: responses: '200': @@ -44,7 +44,14 @@ paths: Register a new provider instance at runtime. The provider will be validated, instantiated, and persisted to the kvstore. Requires appropriate ABAC permissions. - parameters: [] + parameters: + - name: api + in: path + description: >- + API namespace this provider implements (e.g., 'inference', 'vector_io'). + required: true + schema: + type: string requestBody: content: application/json: @@ -52,7 +59,7 @@ paths: $ref: '#/components/schemas/RegisterProviderRequest' required: true deprecated: false - /v1/admin/providers/{provider_id}: + /v1/admin/providers/{api}/{provider_id}: post: responses: '200': @@ -81,10 +88,14 @@ paths: Update the configuration and/or attributes of a dynamic provider. The provider - will be re-instantiated with the new configuration (hot-reload). Static providers - - from run.yaml cannot be updated. + will be re-instantiated with the new configuration (hot-reload). parameters: + - name: api + in: path + description: API namespace the provider implements + required: true + schema: + type: string - name: provider_id in: path description: ID of the provider to update @@ -120,8 +131,14 @@ paths: Remove a dynamic provider, shutting down its instance and removing it from - the kvstore. Static providers from run.yaml cannot be unregistered. + the kvstore. parameters: + - name: api + in: path + description: API namespace the provider implements + required: true + schema: + type: string - name: provider_id in: path description: ID of the provider to unregister. @@ -129,6 +146,47 @@ paths: schema: type: string deprecated: false + /v1/admin/providers/{api}/{provider_id}/test: + post: + responses: + '200': + description: >- + TestProviderConnectionResponse with health status. + content: + application/json: + schema: + $ref: '#/components/schemas/TestProviderConnectionResponse' + '400': + $ref: '#/components/responses/BadRequest400' + '429': + $ref: >- + #/components/responses/TooManyRequests429 + '500': + $ref: >- + #/components/responses/InternalServerError500 + default: + $ref: '#/components/responses/DefaultError' + tags: + - Providers + summary: Test a provider connection. + description: >- + Test a provider connection. + + Execute a health check on a provider to verify it is reachable and functioning. + parameters: + - name: api + in: path + description: API namespace the provider implements. + required: true + schema: + type: string + - name: provider_id + in: path + description: ID of the provider to test. + required: true + schema: + type: string + deprecated: false /v1/chat/completions: get: responses: @@ -1368,7 +1426,43 @@ paths: List all available providers. parameters: [] deprecated: false - /v1/providers/{provider_id}: + /v1/providers/{api}: + get: + responses: + '200': + description: >- + A ListProvidersResponse containing providers for the specified API. + content: + application/json: + schema: + $ref: '#/components/schemas/ListProvidersResponse' + '400': + $ref: '#/components/responses/BadRequest400' + '429': + $ref: >- + #/components/responses/TooManyRequests429 + '500': + $ref: >- + #/components/responses/InternalServerError500 + default: + $ref: '#/components/responses/DefaultError' + tags: + - Providers + summary: List providers for a specific API. + description: >- + List providers for a specific API. + + List all providers that implement a specific API. + parameters: + - name: api + in: path + description: >- + The API namespace to filter by (e.g., 'inference', 'vector_io') + required: true + schema: + type: string + deprecated: false + /v1/providers/{api}/{provider_id}: get: responses: '200': @@ -1390,52 +1484,21 @@ paths: $ref: '#/components/responses/DefaultError' tags: - Providers - summary: Get provider. + summary: Get provider for specific API. description: >- - Get provider. + Get provider for specific API. - Get detailed information about a specific provider. + Get detailed information about a specific provider for a specific API. parameters: - - name: provider_id + - name: api in: path - description: The ID of the provider to inspect. + description: The API namespace. required: true schema: type: string - deprecated: false - /v1/providers/{provider_id}/test: - post: - responses: - '200': - description: >- - TestProviderConnectionResponse with health status. - content: - application/json: - schema: - $ref: '#/components/schemas/TestProviderConnectionResponse' - '400': - $ref: '#/components/responses/BadRequest400' - '429': - $ref: >- - #/components/responses/TooManyRequests429 - '500': - $ref: >- - #/components/responses/InternalServerError500 - default: - $ref: '#/components/responses/DefaultError' - tags: - - Providers - summary: Test a provider connection. - description: >- - Test a provider connection. - - Execute a health check on a provider to verify it is reachable and functioning. - - Works for both static and dynamic providers. - parameters: - name: provider_id in: path - description: ID of the provider to test. + description: The ID of the provider to inspect. required: true schema: type: string @@ -4370,9 +4433,6 @@ components: type: string description: >- Unique identifier for this provider instance. - api: - type: string - description: API namespace this provider implements. provider_type: type: string description: Provider type (e.g., 'remote::openai'). @@ -4399,7 +4459,6 @@ components: additionalProperties: false required: - provider_id - - api - provider_type - config title: RegisterProviderRequest @@ -4608,6 +4667,32 @@ components: - provider title: UpdateProviderResponse description: Response after updating a provider. + TestProviderConnectionResponse: + type: object + properties: + success: + type: boolean + description: Whether the connection test succeeded + health: + type: object + additionalProperties: + oneOf: + - type: 'null' + - type: boolean + - type: number + - type: string + - type: array + - type: object + description: Health status from the provider + error_message: + type: string + description: Error message if test failed + additionalProperties: false + required: + - success + title: TestProviderConnectionResponse + description: >- + Response from testing a provider connection. Order: type: string enum: @@ -7076,32 +7161,6 @@ components: title: ListProvidersResponse description: >- Response containing a list of all available providers. - TestProviderConnectionResponse: - type: object - properties: - success: - type: boolean - description: Whether the connection test succeeded - health: - type: object - additionalProperties: - oneOf: - - type: 'null' - - type: boolean - - type: number - - type: string - - type: array - - type: object - description: Health status from the provider - error_message: - type: string - description: Error message if test failed - additionalProperties: false - required: - - success - title: TestProviderConnectionResponse - description: >- - Response from testing a provider connection. ListOpenAIResponseObject: type: object properties: diff --git a/docs/static/deprecated-llama-stack-spec.html b/docs/static/deprecated-llama-stack-spec.html index 4ae6add60..ba756ba32 100644 --- a/docs/static/deprecated-llama-stack-spec.html +++ b/docs/static/deprecated-llama-stack-spec.html @@ -3526,6 +3526,51 @@ }, "deprecated": true } + }, + "/v1/providers/{provider_id}": { + "get": { + "responses": { + "200": { + "description": "A ListProvidersResponse containing all providers with matching provider_id.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListProvidersResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest400" + }, + "429": { + "$ref": "#/components/responses/TooManyRequests429" + }, + "500": { + "$ref": "#/components/responses/InternalServerError500" + }, + "default": { + "$ref": "#/components/responses/DefaultError" + } + }, + "tags": [ + "Providers" + ], + "summary": "Get providers by ID (deprecated - use /providers/{api}/{provider_id} instead).", + "description": "Get providers by ID (deprecated - use /providers/{api}/{provider_id} instead).\nDEPRECATED: Returns all providers with the given provider_id across all APIs.\nThis can return multiple providers if the same ID is used for different APIs.\nUse /providers/{api}/{provider_id} for unambiguous access.", + "parameters": [ + { + "name": "provider_id", + "in": "path", + "description": "The ID of the provider(s) to inspect.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "deprecated": true + } } }, "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", @@ -13350,6 +13395,103 @@ "logger_config" ], "title": "SupervisedFineTuneRequest" + }, + "ProviderInfo": { + "type": "object", + "properties": { + "api": { + "type": "string", + "description": "The API name this provider implements" + }, + "provider_id": { + "type": "string", + "description": "Unique identifier for the provider" + }, + "provider_type": { + "type": "string", + "description": "The type of provider implementation" + }, + "config": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "array" + }, + { + "type": "object" + } + ] + }, + "description": "Configuration parameters for the provider" + }, + "health": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "array" + }, + { + "type": "object" + } + ] + }, + "description": "Current health status of the provider" + } + }, + "additionalProperties": false, + "required": [ + "api", + "provider_id", + "provider_type", + "config", + "health" + ], + "title": "ProviderInfo", + "description": "Information about a registered provider including its configuration and health status." + }, + "ListProvidersResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProviderInfo" + }, + "description": "List of provider information objects" + } + }, + "additionalProperties": false, + "required": [ + "data" + ], + "title": "ListProvidersResponse", + "description": "Response containing a list of all available providers." } }, "responses": { @@ -13461,6 +13603,11 @@ "name": "PostTraining (Coming Soon)", "description": "" }, + { + "name": "Providers", + "description": "Providers API for inspecting, listing, and modifying providers and their configurations.", + "x-displayName": "Providers" + }, { "name": "Safety", "description": "OpenAI-compatible Moderations API.", @@ -13484,6 +13631,7 @@ "Inference", "Models", "PostTraining (Coming Soon)", + "Providers", "Safety", "VectorIO" ] diff --git a/docs/static/deprecated-llama-stack-spec.yaml b/docs/static/deprecated-llama-stack-spec.yaml index 3bcfde02e..cea7956cf 100644 --- a/docs/static/deprecated-llama-stack-spec.yaml +++ b/docs/static/deprecated-llama-stack-spec.yaml @@ -2600,6 +2600,46 @@ paths: $ref: '#/components/schemas/SupervisedFineTuneRequest' required: true deprecated: true + /v1/providers/{provider_id}: + get: + responses: + '200': + description: >- + A ListProvidersResponse containing all providers with matching provider_id. + content: + application/json: + schema: + $ref: '#/components/schemas/ListProvidersResponse' + '400': + $ref: '#/components/responses/BadRequest400' + '429': + $ref: >- + #/components/responses/TooManyRequests429 + '500': + $ref: >- + #/components/responses/InternalServerError500 + default: + $ref: '#/components/responses/DefaultError' + tags: + - Providers + summary: >- + Get providers by ID (deprecated - use /providers/{api}/{provider_id} instead). + description: >- + Get providers by ID (deprecated - use /providers/{api}/{provider_id} instead). + + DEPRECATED: Returns all providers with the given provider_id across all APIs. + + This can return multiple providers if the same ID is used for different APIs. + + Use /providers/{api}/{provider_id} for unambiguous access. + parameters: + - name: provider_id + in: path + description: The ID of the provider(s) to inspect. + required: true + schema: + type: string + deprecated: true jsonSchemaDialect: >- https://json-schema.org/draft/2020-12/schema components: @@ -10121,6 +10161,66 @@ components: - hyperparam_search_config - logger_config title: SupervisedFineTuneRequest + ProviderInfo: + type: object + properties: + api: + type: string + description: The API name this provider implements + provider_id: + type: string + description: Unique identifier for the provider + provider_type: + type: string + description: The type of provider implementation + config: + type: object + additionalProperties: + oneOf: + - type: 'null' + - type: boolean + - type: number + - type: string + - type: array + - type: object + description: >- + Configuration parameters for the provider + health: + type: object + additionalProperties: + oneOf: + - type: 'null' + - type: boolean + - type: number + - type: string + - type: array + - type: object + description: Current health status of the provider + additionalProperties: false + required: + - api + - provider_id + - provider_type + - config + - health + title: ProviderInfo + description: >- + Information about a registered provider including its configuration and health + status. + ListProvidersResponse: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/ProviderInfo' + description: List of provider information objects + additionalProperties: false + required: + - data + title: ListProvidersResponse + description: >- + Response containing a list of all available providers. responses: BadRequest400: description: The request was invalid or malformed @@ -10226,6 +10326,10 @@ tags: description: '' - name: PostTraining (Coming Soon) description: '' + - name: Providers + description: >- + Providers API for inspecting, listing, and modifying providers and their configurations. + x-displayName: Providers - name: Safety description: OpenAI-compatible Moderations API. x-displayName: Safety @@ -10243,5 +10347,6 @@ x-tagGroups: - Inference - Models - PostTraining (Coming Soon) + - Providers - Safety - VectorIO diff --git a/docs/static/llama-stack-spec.html b/docs/static/llama-stack-spec.html index 8df813176..864cf118a 100644 --- a/docs/static/llama-stack-spec.html +++ b/docs/static/llama-stack-spec.html @@ -40,7 +40,7 @@ } ], "paths": { - "/v1/admin/providers": { + "/v1/admin/providers/{api}": { "post": { "responses": { "200": { @@ -71,7 +71,17 @@ ], "summary": "Register a new dynamic provider.", "description": "Register a new dynamic provider.\nRegister a new provider instance at runtime. The provider will be validated,\ninstantiated, and persisted to the kvstore. Requires appropriate ABAC permissions.", - "parameters": [], + "parameters": [ + { + "name": "api", + "in": "path", + "description": "API namespace this provider implements (e.g., 'inference', 'vector_io').", + "required": true, + "schema": { + "type": "string" + } + } + ], "requestBody": { "content": { "application/json": { @@ -85,7 +95,7 @@ "deprecated": false } }, - "/v1/admin/providers/{provider_id}": { + "/v1/admin/providers/{api}/{provider_id}": { "post": { "responses": { "200": { @@ -115,8 +125,17 @@ "Providers" ], "summary": "Update an existing provider's configuration.", - "description": "Update an existing provider's configuration.\nUpdate the configuration and/or attributes of a dynamic provider. The provider\nwill be re-instantiated with the new configuration (hot-reload). Static providers\nfrom run.yaml cannot be updated.", + "description": "Update an existing provider's configuration.\nUpdate the configuration and/or attributes of a dynamic provider. The provider\nwill be re-instantiated with the new configuration (hot-reload).", "parameters": [ + { + "name": "api", + "in": "path", + "description": "API namespace the provider implements", + "required": true, + "schema": { + "type": "string" + } + }, { "name": "provider_id", "in": "path", @@ -161,8 +180,17 @@ "Providers" ], "summary": "Unregister a dynamic provider.", - "description": "Unregister a dynamic provider.\nRemove a dynamic provider, shutting down its instance and removing it from\nthe kvstore. Static providers from run.yaml cannot be unregistered.", + "description": "Unregister a dynamic provider.\nRemove a dynamic provider, shutting down its instance and removing it from\nthe kvstore.", "parameters": [ + { + "name": "api", + "in": "path", + "description": "API namespace the provider implements", + "required": true, + "schema": { + "type": "string" + } + }, { "name": "provider_id", "in": "path", @@ -176,6 +204,60 @@ "deprecated": false } }, + "/v1/admin/providers/{api}/{provider_id}/test": { + "post": { + "responses": { + "200": { + "description": "TestProviderConnectionResponse with health status.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestProviderConnectionResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest400" + }, + "429": { + "$ref": "#/components/responses/TooManyRequests429" + }, + "500": { + "$ref": "#/components/responses/InternalServerError500" + }, + "default": { + "$ref": "#/components/responses/DefaultError" + } + }, + "tags": [ + "Providers" + ], + "summary": "Test a provider connection.", + "description": "Test a provider connection.\nExecute a health check on a provider to verify it is reachable and functioning.", + "parameters": [ + { + "name": "api", + "in": "path", + "description": "API namespace the provider implements.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "provider_id", + "in": "path", + "description": "ID of the provider to test.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "deprecated": false + } + }, "/v1/chat/completions": { "get": { "responses": { @@ -1771,7 +1853,52 @@ "deprecated": false } }, - "/v1/providers/{provider_id}": { + "/v1/providers/{api}": { + "get": { + "responses": { + "200": { + "description": "A ListProvidersResponse containing providers for the specified API.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListProvidersResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest400" + }, + "429": { + "$ref": "#/components/responses/TooManyRequests429" + }, + "500": { + "$ref": "#/components/responses/InternalServerError500" + }, + "default": { + "$ref": "#/components/responses/DefaultError" + } + }, + "tags": [ + "Providers" + ], + "summary": "List providers for a specific API.", + "description": "List providers for a specific API.\nList all providers that implement a specific API.", + "parameters": [ + { + "name": "api", + "in": "path", + "description": "The API namespace to filter by (e.g., 'inference', 'vector_io')", + "required": true, + "schema": { + "type": "string" + } + } + ], + "deprecated": false + } + }, + "/v1/providers/{api}/{provider_id}": { "get": { "responses": { "200": { @@ -1800,58 +1927,22 @@ "tags": [ "Providers" ], - "summary": "Get provider.", - "description": "Get provider.\nGet detailed information about a specific provider.", + "summary": "Get provider for specific API.", + "description": "Get provider for specific API.\nGet detailed information about a specific provider for a specific API.", "parameters": [ { - "name": "provider_id", + "name": "api", "in": "path", - "description": "The ID of the provider to inspect.", + "description": "The API namespace.", "required": true, "schema": { "type": "string" } - } - ], - "deprecated": false - } - }, - "/v1/providers/{provider_id}/test": { - "post": { - "responses": { - "200": { - "description": "TestProviderConnectionResponse with health status.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TestProviderConnectionResponse" - } - } - } }, - "400": { - "$ref": "#/components/responses/BadRequest400" - }, - "429": { - "$ref": "#/components/responses/TooManyRequests429" - }, - "500": { - "$ref": "#/components/responses/InternalServerError500" - }, - "default": { - "$ref": "#/components/responses/DefaultError" - } - }, - "tags": [ - "Providers" - ], - "summary": "Test a provider connection.", - "description": "Test a provider connection.\nExecute a health check on a provider to verify it is reachable and functioning.\nWorks for both static and dynamic providers.", - "parameters": [ { "name": "provider_id", "in": "path", - "description": "ID of the provider to test.", + "description": "The ID of the provider to inspect.", "required": true, "schema": { "type": "string" @@ -4193,10 +4284,6 @@ "type": "string", "description": "Unique identifier for this provider instance." }, - "api": { - "type": "string", - "description": "API namespace this provider implements." - }, "provider_type": { "type": "string", "description": "Provider type (e.g., 'remote::openai')." @@ -4241,7 +4328,6 @@ "additionalProperties": false, "required": [ "provider_id", - "api", "provider_type", "config" ], @@ -4531,6 +4617,51 @@ "title": "UpdateProviderResponse", "description": "Response after updating a provider." }, + "TestProviderConnectionResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "description": "Whether the connection test succeeded" + }, + "health": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "array" + }, + { + "type": "object" + } + ] + }, + "description": "Health status from the provider" + }, + "error_message": { + "type": "string", + "description": "Error message if test failed" + } + }, + "additionalProperties": false, + "required": [ + "success" + ], + "title": "TestProviderConnectionResponse", + "description": "Response from testing a provider connection." + }, "Order": { "type": "string", "enum": [ @@ -7768,51 +7899,6 @@ "title": "ListProvidersResponse", "description": "Response containing a list of all available providers." }, - "TestProviderConnectionResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "Whether the connection test succeeded" - }, - "health": { - "type": "object", - "additionalProperties": { - "oneOf": [ - { - "type": "null" - }, - { - "type": "boolean" - }, - { - "type": "number" - }, - { - "type": "string" - }, - { - "type": "array" - }, - { - "type": "object" - } - ] - }, - "description": "Health status from the provider" - }, - "error_message": { - "type": "string", - "description": "Error message if test failed" - } - }, - "additionalProperties": false, - "required": [ - "success" - ], - "title": "TestProviderConnectionResponse", - "description": "Response from testing a provider connection." - }, "ListOpenAIResponseObject": { "type": "object", "properties": { diff --git a/docs/static/llama-stack-spec.yaml b/docs/static/llama-stack-spec.yaml index 4eafa60e5..eab53a309 100644 --- a/docs/static/llama-stack-spec.yaml +++ b/docs/static/llama-stack-spec.yaml @@ -12,7 +12,7 @@ info: servers: - url: http://any-hosted-llama-stack.com paths: - /v1/admin/providers: + /v1/admin/providers/{api}: post: responses: '200': @@ -41,7 +41,14 @@ paths: Register a new provider instance at runtime. The provider will be validated, instantiated, and persisted to the kvstore. Requires appropriate ABAC permissions. - parameters: [] + parameters: + - name: api + in: path + description: >- + API namespace this provider implements (e.g., 'inference', 'vector_io'). + required: true + schema: + type: string requestBody: content: application/json: @@ -49,7 +56,7 @@ paths: $ref: '#/components/schemas/RegisterProviderRequest' required: true deprecated: false - /v1/admin/providers/{provider_id}: + /v1/admin/providers/{api}/{provider_id}: post: responses: '200': @@ -78,10 +85,14 @@ paths: Update the configuration and/or attributes of a dynamic provider. The provider - will be re-instantiated with the new configuration (hot-reload). Static providers - - from run.yaml cannot be updated. + will be re-instantiated with the new configuration (hot-reload). parameters: + - name: api + in: path + description: API namespace the provider implements + required: true + schema: + type: string - name: provider_id in: path description: ID of the provider to update @@ -117,8 +128,14 @@ paths: Remove a dynamic provider, shutting down its instance and removing it from - the kvstore. Static providers from run.yaml cannot be unregistered. + the kvstore. parameters: + - name: api + in: path + description: API namespace the provider implements + required: true + schema: + type: string - name: provider_id in: path description: ID of the provider to unregister. @@ -126,6 +143,47 @@ paths: schema: type: string deprecated: false + /v1/admin/providers/{api}/{provider_id}/test: + post: + responses: + '200': + description: >- + TestProviderConnectionResponse with health status. + content: + application/json: + schema: + $ref: '#/components/schemas/TestProviderConnectionResponse' + '400': + $ref: '#/components/responses/BadRequest400' + '429': + $ref: >- + #/components/responses/TooManyRequests429 + '500': + $ref: >- + #/components/responses/InternalServerError500 + default: + $ref: '#/components/responses/DefaultError' + tags: + - Providers + summary: Test a provider connection. + description: >- + Test a provider connection. + + Execute a health check on a provider to verify it is reachable and functioning. + parameters: + - name: api + in: path + description: API namespace the provider implements. + required: true + schema: + type: string + - name: provider_id + in: path + description: ID of the provider to test. + required: true + schema: + type: string + deprecated: false /v1/chat/completions: get: responses: @@ -1365,7 +1423,43 @@ paths: List all available providers. parameters: [] deprecated: false - /v1/providers/{provider_id}: + /v1/providers/{api}: + get: + responses: + '200': + description: >- + A ListProvidersResponse containing providers for the specified API. + content: + application/json: + schema: + $ref: '#/components/schemas/ListProvidersResponse' + '400': + $ref: '#/components/responses/BadRequest400' + '429': + $ref: >- + #/components/responses/TooManyRequests429 + '500': + $ref: >- + #/components/responses/InternalServerError500 + default: + $ref: '#/components/responses/DefaultError' + tags: + - Providers + summary: List providers for a specific API. + description: >- + List providers for a specific API. + + List all providers that implement a specific API. + parameters: + - name: api + in: path + description: >- + The API namespace to filter by (e.g., 'inference', 'vector_io') + required: true + schema: + type: string + deprecated: false + /v1/providers/{api}/{provider_id}: get: responses: '200': @@ -1387,52 +1481,21 @@ paths: $ref: '#/components/responses/DefaultError' tags: - Providers - summary: Get provider. + summary: Get provider for specific API. description: >- - Get provider. + Get provider for specific API. - Get detailed information about a specific provider. + Get detailed information about a specific provider for a specific API. parameters: - - name: provider_id + - name: api in: path - description: The ID of the provider to inspect. + description: The API namespace. required: true schema: type: string - deprecated: false - /v1/providers/{provider_id}/test: - post: - responses: - '200': - description: >- - TestProviderConnectionResponse with health status. - content: - application/json: - schema: - $ref: '#/components/schemas/TestProviderConnectionResponse' - '400': - $ref: '#/components/responses/BadRequest400' - '429': - $ref: >- - #/components/responses/TooManyRequests429 - '500': - $ref: >- - #/components/responses/InternalServerError500 - default: - $ref: '#/components/responses/DefaultError' - tags: - - Providers - summary: Test a provider connection. - description: >- - Test a provider connection. - - Execute a health check on a provider to verify it is reachable and functioning. - - Works for both static and dynamic providers. - parameters: - name: provider_id in: path - description: ID of the provider to test. + description: The ID of the provider to inspect. required: true schema: type: string @@ -3157,9 +3220,6 @@ components: type: string description: >- Unique identifier for this provider instance. - api: - type: string - description: API namespace this provider implements. provider_type: type: string description: Provider type (e.g., 'remote::openai'). @@ -3186,7 +3246,6 @@ components: additionalProperties: false required: - provider_id - - api - provider_type - config title: RegisterProviderRequest @@ -3395,6 +3454,32 @@ components: - provider title: UpdateProviderResponse description: Response after updating a provider. + TestProviderConnectionResponse: + type: object + properties: + success: + type: boolean + description: Whether the connection test succeeded + health: + type: object + additionalProperties: + oneOf: + - type: 'null' + - type: boolean + - type: number + - type: string + - type: array + - type: object + description: Health status from the provider + error_message: + type: string + description: Error message if test failed + additionalProperties: false + required: + - success + title: TestProviderConnectionResponse + description: >- + Response from testing a provider connection. Order: type: string enum: @@ -5863,32 +5948,6 @@ components: title: ListProvidersResponse description: >- Response containing a list of all available providers. - TestProviderConnectionResponse: - type: object - properties: - success: - type: boolean - description: Whether the connection test succeeded - health: - type: object - additionalProperties: - oneOf: - - type: 'null' - - type: boolean - - type: number - - type: string - - type: array - - type: object - description: Health status from the provider - error_message: - type: string - description: Error message if test failed - additionalProperties: false - required: - - success - title: TestProviderConnectionResponse - description: >- - Response from testing a provider connection. ListOpenAIResponseObject: type: object properties: diff --git a/docs/static/stainless-llama-stack-spec.html b/docs/static/stainless-llama-stack-spec.html index 0ce5be819..ea66ecad7 100644 --- a/docs/static/stainless-llama-stack-spec.html +++ b/docs/static/stainless-llama-stack-spec.html @@ -40,7 +40,7 @@ } ], "paths": { - "/v1/admin/providers": { + "/v1/admin/providers/{api}": { "post": { "responses": { "200": { @@ -71,7 +71,17 @@ ], "summary": "Register a new dynamic provider.", "description": "Register a new dynamic provider.\nRegister a new provider instance at runtime. The provider will be validated,\ninstantiated, and persisted to the kvstore. Requires appropriate ABAC permissions.", - "parameters": [], + "parameters": [ + { + "name": "api", + "in": "path", + "description": "API namespace this provider implements (e.g., 'inference', 'vector_io').", + "required": true, + "schema": { + "type": "string" + } + } + ], "requestBody": { "content": { "application/json": { @@ -85,7 +95,7 @@ "deprecated": false } }, - "/v1/admin/providers/{provider_id}": { + "/v1/admin/providers/{api}/{provider_id}": { "post": { "responses": { "200": { @@ -115,8 +125,17 @@ "Providers" ], "summary": "Update an existing provider's configuration.", - "description": "Update an existing provider's configuration.\nUpdate the configuration and/or attributes of a dynamic provider. The provider\nwill be re-instantiated with the new configuration (hot-reload). Static providers\nfrom run.yaml cannot be updated.", + "description": "Update an existing provider's configuration.\nUpdate the configuration and/or attributes of a dynamic provider. The provider\nwill be re-instantiated with the new configuration (hot-reload).", "parameters": [ + { + "name": "api", + "in": "path", + "description": "API namespace the provider implements", + "required": true, + "schema": { + "type": "string" + } + }, { "name": "provider_id", "in": "path", @@ -161,8 +180,17 @@ "Providers" ], "summary": "Unregister a dynamic provider.", - "description": "Unregister a dynamic provider.\nRemove a dynamic provider, shutting down its instance and removing it from\nthe kvstore. Static providers from run.yaml cannot be unregistered.", + "description": "Unregister a dynamic provider.\nRemove a dynamic provider, shutting down its instance and removing it from\nthe kvstore.", "parameters": [ + { + "name": "api", + "in": "path", + "description": "API namespace the provider implements", + "required": true, + "schema": { + "type": "string" + } + }, { "name": "provider_id", "in": "path", @@ -176,6 +204,60 @@ "deprecated": false } }, + "/v1/admin/providers/{api}/{provider_id}/test": { + "post": { + "responses": { + "200": { + "description": "TestProviderConnectionResponse with health status.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestProviderConnectionResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest400" + }, + "429": { + "$ref": "#/components/responses/TooManyRequests429" + }, + "500": { + "$ref": "#/components/responses/InternalServerError500" + }, + "default": { + "$ref": "#/components/responses/DefaultError" + } + }, + "tags": [ + "Providers" + ], + "summary": "Test a provider connection.", + "description": "Test a provider connection.\nExecute a health check on a provider to verify it is reachable and functioning.", + "parameters": [ + { + "name": "api", + "in": "path", + "description": "API namespace the provider implements.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "provider_id", + "in": "path", + "description": "ID of the provider to test.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "deprecated": false + } + }, "/v1/chat/completions": { "get": { "responses": { @@ -1771,7 +1853,52 @@ "deprecated": false } }, - "/v1/providers/{provider_id}": { + "/v1/providers/{api}": { + "get": { + "responses": { + "200": { + "description": "A ListProvidersResponse containing providers for the specified API.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListProvidersResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest400" + }, + "429": { + "$ref": "#/components/responses/TooManyRequests429" + }, + "500": { + "$ref": "#/components/responses/InternalServerError500" + }, + "default": { + "$ref": "#/components/responses/DefaultError" + } + }, + "tags": [ + "Providers" + ], + "summary": "List providers for a specific API.", + "description": "List providers for a specific API.\nList all providers that implement a specific API.", + "parameters": [ + { + "name": "api", + "in": "path", + "description": "The API namespace to filter by (e.g., 'inference', 'vector_io')", + "required": true, + "schema": { + "type": "string" + } + } + ], + "deprecated": false + } + }, + "/v1/providers/{api}/{provider_id}": { "get": { "responses": { "200": { @@ -1800,58 +1927,22 @@ "tags": [ "Providers" ], - "summary": "Get provider.", - "description": "Get provider.\nGet detailed information about a specific provider.", + "summary": "Get provider for specific API.", + "description": "Get provider for specific API.\nGet detailed information about a specific provider for a specific API.", "parameters": [ { - "name": "provider_id", + "name": "api", "in": "path", - "description": "The ID of the provider to inspect.", + "description": "The API namespace.", "required": true, "schema": { "type": "string" } - } - ], - "deprecated": false - } - }, - "/v1/providers/{provider_id}/test": { - "post": { - "responses": { - "200": { - "description": "TestProviderConnectionResponse with health status.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TestProviderConnectionResponse" - } - } - } }, - "400": { - "$ref": "#/components/responses/BadRequest400" - }, - "429": { - "$ref": "#/components/responses/TooManyRequests429" - }, - "500": { - "$ref": "#/components/responses/InternalServerError500" - }, - "default": { - "$ref": "#/components/responses/DefaultError" - } - }, - "tags": [ - "Providers" - ], - "summary": "Test a provider connection.", - "description": "Test a provider connection.\nExecute a health check on a provider to verify it is reachable and functioning.\nWorks for both static and dynamic providers.", - "parameters": [ { "name": "provider_id", "in": "path", - "description": "ID of the provider to test.", + "description": "The ID of the provider to inspect.", "required": true, "schema": { "type": "string" @@ -5865,10 +5956,6 @@ "type": "string", "description": "Unique identifier for this provider instance." }, - "api": { - "type": "string", - "description": "API namespace this provider implements." - }, "provider_type": { "type": "string", "description": "Provider type (e.g., 'remote::openai')." @@ -5913,7 +6000,6 @@ "additionalProperties": false, "required": [ "provider_id", - "api", "provider_type", "config" ], @@ -6203,6 +6289,51 @@ "title": "UpdateProviderResponse", "description": "Response after updating a provider." }, + "TestProviderConnectionResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "description": "Whether the connection test succeeded" + }, + "health": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "array" + }, + { + "type": "object" + } + ] + }, + "description": "Health status from the provider" + }, + "error_message": { + "type": "string", + "description": "Error message if test failed" + } + }, + "additionalProperties": false, + "required": [ + "success" + ], + "title": "TestProviderConnectionResponse", + "description": "Response from testing a provider connection." + }, "Order": { "type": "string", "enum": [ @@ -9440,51 +9571,6 @@ "title": "ListProvidersResponse", "description": "Response containing a list of all available providers." }, - "TestProviderConnectionResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "Whether the connection test succeeded" - }, - "health": { - "type": "object", - "additionalProperties": { - "oneOf": [ - { - "type": "null" - }, - { - "type": "boolean" - }, - { - "type": "number" - }, - { - "type": "string" - }, - { - "type": "array" - }, - { - "type": "object" - } - ] - }, - "description": "Health status from the provider" - }, - "error_message": { - "type": "string", - "description": "Error message if test failed" - } - }, - "additionalProperties": false, - "required": [ - "success" - ], - "title": "TestProviderConnectionResponse", - "description": "Response from testing a provider connection." - }, "ListOpenAIResponseObject": { "type": "object", "properties": { diff --git a/docs/static/stainless-llama-stack-spec.yaml b/docs/static/stainless-llama-stack-spec.yaml index 2953055a5..e05d6eba1 100644 --- a/docs/static/stainless-llama-stack-spec.yaml +++ b/docs/static/stainless-llama-stack-spec.yaml @@ -15,7 +15,7 @@ info: servers: - url: http://any-hosted-llama-stack.com paths: - /v1/admin/providers: + /v1/admin/providers/{api}: post: responses: '200': @@ -44,7 +44,14 @@ paths: Register a new provider instance at runtime. The provider will be validated, instantiated, and persisted to the kvstore. Requires appropriate ABAC permissions. - parameters: [] + parameters: + - name: api + in: path + description: >- + API namespace this provider implements (e.g., 'inference', 'vector_io'). + required: true + schema: + type: string requestBody: content: application/json: @@ -52,7 +59,7 @@ paths: $ref: '#/components/schemas/RegisterProviderRequest' required: true deprecated: false - /v1/admin/providers/{provider_id}: + /v1/admin/providers/{api}/{provider_id}: post: responses: '200': @@ -81,10 +88,14 @@ paths: Update the configuration and/or attributes of a dynamic provider. The provider - will be re-instantiated with the new configuration (hot-reload). Static providers - - from run.yaml cannot be updated. + will be re-instantiated with the new configuration (hot-reload). parameters: + - name: api + in: path + description: API namespace the provider implements + required: true + schema: + type: string - name: provider_id in: path description: ID of the provider to update @@ -120,8 +131,14 @@ paths: Remove a dynamic provider, shutting down its instance and removing it from - the kvstore. Static providers from run.yaml cannot be unregistered. + the kvstore. parameters: + - name: api + in: path + description: API namespace the provider implements + required: true + schema: + type: string - name: provider_id in: path description: ID of the provider to unregister. @@ -129,6 +146,47 @@ paths: schema: type: string deprecated: false + /v1/admin/providers/{api}/{provider_id}/test: + post: + responses: + '200': + description: >- + TestProviderConnectionResponse with health status. + content: + application/json: + schema: + $ref: '#/components/schemas/TestProviderConnectionResponse' + '400': + $ref: '#/components/responses/BadRequest400' + '429': + $ref: >- + #/components/responses/TooManyRequests429 + '500': + $ref: >- + #/components/responses/InternalServerError500 + default: + $ref: '#/components/responses/DefaultError' + tags: + - Providers + summary: Test a provider connection. + description: >- + Test a provider connection. + + Execute a health check on a provider to verify it is reachable and functioning. + parameters: + - name: api + in: path + description: API namespace the provider implements. + required: true + schema: + type: string + - name: provider_id + in: path + description: ID of the provider to test. + required: true + schema: + type: string + deprecated: false /v1/chat/completions: get: responses: @@ -1368,7 +1426,43 @@ paths: List all available providers. parameters: [] deprecated: false - /v1/providers/{provider_id}: + /v1/providers/{api}: + get: + responses: + '200': + description: >- + A ListProvidersResponse containing providers for the specified API. + content: + application/json: + schema: + $ref: '#/components/schemas/ListProvidersResponse' + '400': + $ref: '#/components/responses/BadRequest400' + '429': + $ref: >- + #/components/responses/TooManyRequests429 + '500': + $ref: >- + #/components/responses/InternalServerError500 + default: + $ref: '#/components/responses/DefaultError' + tags: + - Providers + summary: List providers for a specific API. + description: >- + List providers for a specific API. + + List all providers that implement a specific API. + parameters: + - name: api + in: path + description: >- + The API namespace to filter by (e.g., 'inference', 'vector_io') + required: true + schema: + type: string + deprecated: false + /v1/providers/{api}/{provider_id}: get: responses: '200': @@ -1390,52 +1484,21 @@ paths: $ref: '#/components/responses/DefaultError' tags: - Providers - summary: Get provider. + summary: Get provider for specific API. description: >- - Get provider. + Get provider for specific API. - Get detailed information about a specific provider. + Get detailed information about a specific provider for a specific API. parameters: - - name: provider_id + - name: api in: path - description: The ID of the provider to inspect. + description: The API namespace. required: true schema: type: string - deprecated: false - /v1/providers/{provider_id}/test: - post: - responses: - '200': - description: >- - TestProviderConnectionResponse with health status. - content: - application/json: - schema: - $ref: '#/components/schemas/TestProviderConnectionResponse' - '400': - $ref: '#/components/responses/BadRequest400' - '429': - $ref: >- - #/components/responses/TooManyRequests429 - '500': - $ref: >- - #/components/responses/InternalServerError500 - default: - $ref: '#/components/responses/DefaultError' - tags: - - Providers - summary: Test a provider connection. - description: >- - Test a provider connection. - - Execute a health check on a provider to verify it is reachable and functioning. - - Works for both static and dynamic providers. - parameters: - name: provider_id in: path - description: ID of the provider to test. + description: The ID of the provider to inspect. required: true schema: type: string @@ -4370,9 +4433,6 @@ components: type: string description: >- Unique identifier for this provider instance. - api: - type: string - description: API namespace this provider implements. provider_type: type: string description: Provider type (e.g., 'remote::openai'). @@ -4399,7 +4459,6 @@ components: additionalProperties: false required: - provider_id - - api - provider_type - config title: RegisterProviderRequest @@ -4608,6 +4667,32 @@ components: - provider title: UpdateProviderResponse description: Response after updating a provider. + TestProviderConnectionResponse: + type: object + properties: + success: + type: boolean + description: Whether the connection test succeeded + health: + type: object + additionalProperties: + oneOf: + - type: 'null' + - type: boolean + - type: number + - type: string + - type: array + - type: object + description: Health status from the provider + error_message: + type: string + description: Error message if test failed + additionalProperties: false + required: + - success + title: TestProviderConnectionResponse + description: >- + Response from testing a provider connection. Order: type: string enum: @@ -7076,32 +7161,6 @@ components: title: ListProvidersResponse description: >- Response containing a list of all available providers. - TestProviderConnectionResponse: - type: object - properties: - success: - type: boolean - description: Whether the connection test succeeded - health: - type: object - additionalProperties: - oneOf: - - type: 'null' - - type: boolean - - type: number - - type: string - - type: array - - type: object - description: Health status from the provider - error_message: - type: string - description: Error message if test failed - additionalProperties: false - required: - - success - title: TestProviderConnectionResponse - description: >- - Response from testing a provider connection. ListOpenAIResponseObject: type: object properties: diff --git a/llama_stack/apis/providers/providers.py b/llama_stack/apis/providers/providers.py index c52a15e0c..38d7c4843 100644 --- a/llama_stack/apis/providers/providers.py +++ b/llama_stack/apis/providers/providers.py @@ -137,24 +137,26 @@ class Providers(Protocol): """ ... - @webmethod(route="/providers/{provider_id}", method="GET", level=LLAMA_STACK_API_V1) - async def inspect_provider(self, provider_id: str) -> ProviderInfo: - """Get provider. + @webmethod(route="/providers/{provider_id}", method="GET", level=LLAMA_STACK_API_V1, deprecated=True) + async def inspect_provider(self, provider_id: str) -> ListProvidersResponse: + """Get providers by ID (deprecated - use /providers/{api}/{provider_id} instead). - Get detailed information about a specific provider. + DEPRECATED: Returns all providers with the given provider_id across all APIs. + This can return multiple providers if the same ID is used for different APIs. + Use /providers/{api}/{provider_id} for unambiguous access. - :param provider_id: The ID of the provider to inspect. - :returns: A ProviderInfo object containing the provider's details. + :param provider_id: The ID of the provider(s) to inspect. + :returns: A ListProvidersResponse containing all providers with matching provider_id. """ ... # ===== Dynamic Provider Management Methods ===== - @webmethod(route="/admin/providers", method="POST", level=LLAMA_STACK_API_V1) + @webmethod(route="/admin/providers/{api}", method="POST", level=LLAMA_STACK_API_V1) async def register_provider( self, - provider_id: str, api: str, + provider_id: str, provider_type: str, config: dict[str, Any], attributes: dict[str, list[str]] | None = None, @@ -164,8 +166,8 @@ class Providers(Protocol): Register a new provider instance at runtime. The provider will be validated, instantiated, and persisted to the kvstore. Requires appropriate ABAC permissions. + :param api: API namespace this provider implements (e.g., 'inference', 'vector_io'). :param provider_id: Unique identifier for this provider instance. - :param api: API namespace this provider implements. :param provider_type: Provider type (e.g., 'remote::openai'). :param config: Provider configuration (API keys, endpoints, etc.). :param attributes: Optional attributes for ABAC access control. @@ -173,9 +175,10 @@ class Providers(Protocol): """ ... - @webmethod(route="/admin/providers/{provider_id}", method="PUT", level=LLAMA_STACK_API_V1) + @webmethod(route="/admin/providers/{api}/{provider_id}", method="PUT", level=LLAMA_STACK_API_V1) async def update_provider( self, + api: str, provider_id: str, config: dict[str, Any] | None = None, attributes: dict[str, list[str]] | None = None, @@ -183,9 +186,9 @@ class Providers(Protocol): """Update an existing provider's configuration. Update the configuration and/or attributes of a dynamic provider. The provider - will be re-instantiated with the new configuration (hot-reload). Static providers - from run.yaml cannot be updated. + will be re-instantiated with the new configuration (hot-reload). + :param api: API namespace the provider implements :param provider_id: ID of the provider to update :param config: New configuration parameters (merged with existing) :param attributes: New attributes for access control @@ -193,25 +196,49 @@ class Providers(Protocol): """ ... - @webmethod(route="/admin/providers/{provider_id}", method="DELETE", level=LLAMA_STACK_API_V1) - async def unregister_provider(self, provider_id: str) -> None: + @webmethod(route="/admin/providers/{api}/{provider_id}", method="DELETE", level=LLAMA_STACK_API_V1) + async def unregister_provider(self, api: str, provider_id: str) -> None: """Unregister a dynamic provider. Remove a dynamic provider, shutting down its instance and removing it from - the kvstore. Static providers from run.yaml cannot be unregistered. + the kvstore. + :param api: API namespace the provider implements :param provider_id: ID of the provider to unregister. """ ... - @webmethod(route="/providers/{provider_id}/test", method="POST", level=LLAMA_STACK_API_V1) - async def test_provider_connection(self, provider_id: str) -> TestProviderConnectionResponse: + @webmethod(route="/admin/providers/{api}/{provider_id}/test", method="POST", level=LLAMA_STACK_API_V1) + async def test_provider_connection(self, api: str, provider_id: str) -> TestProviderConnectionResponse: """Test a provider connection. Execute a health check on a provider to verify it is reachable and functioning. - Works for both static and dynamic providers. + :param api: API namespace the provider implements. :param provider_id: ID of the provider to test. :returns: TestProviderConnectionResponse with health status. """ ... + + @webmethod(route="/providers/{api}", method="GET", level=LLAMA_STACK_API_V1) + async def list_providers_for_api(self, api: str) -> ListProvidersResponse: + """List providers for a specific API. + + List all providers that implement a specific API. + + :param api: The API namespace to filter by (e.g., 'inference', 'vector_io') + :returns: A ListProvidersResponse containing providers for the specified API. + """ + ... + + @webmethod(route="/providers/{api}/{provider_id}", method="GET", level=LLAMA_STACK_API_V1) + async def inspect_provider_for_api(self, api: str, provider_id: str) -> ProviderInfo: + """Get provider for specific API. + + Get detailed information about a specific provider for a specific API. + + :param api: The API namespace. + :param provider_id: The ID of the provider to inspect. + :returns: A ProviderInfo object containing the provider's details. + """ + ... diff --git a/llama_stack/core/providers.py b/llama_stack/core/providers.py index 9a0a478c2..aa1834d46 100644 --- a/llama_stack/core/providers.py +++ b/llama_stack/core/providers.py @@ -79,29 +79,24 @@ class ProviderImpl(Providers): from llama_stack.providers.utils.kvstore import kvstore_impl self.kvstore = await kvstore_impl(self.config.run_config.storage.stores.metadata) - logger.info("✅ Initialized kvstore for dynamic provider management") + logger.info("Initialized kvstore for dynamic provider management") # Load existing dynamic providers from kvstore await self._load_dynamic_providers() - logger.info(f"📦 Loaded {len(self.dynamic_providers)} existing dynamic providers from kvstore") + logger.info(f"Loaded {len(self.dynamic_providers)} existing dynamic providers from kvstore") - # Auto-instantiate connected providers on startup - if self.provider_registry: - for provider_id, conn_info in self.dynamic_providers.items(): - if conn_info.status == ProviderConnectionStatus.connected: - try: - impl = await self._instantiate_provider(conn_info) - self.dynamic_provider_impls[provider_id] = impl - logger.info(f"♻️ Auto-instantiated provider {provider_id} from kvstore") - except Exception as e: - logger.error(f"Failed to auto-instantiate provider {provider_id}: {e}") - # Update status to failed - conn_info.status = ProviderConnectionStatus.failed - conn_info.error_message = str(e) - conn_info.updated_at = datetime.now(UTC) - await self._store_connection(conn_info) - else: - logger.warning("Provider registry not available, skipping auto-instantiation") + for provider_id, conn_info in self.dynamic_providers.items(): + if conn_info.status == ProviderConnectionStatus.connected: + try: + impl = await self._instantiate_provider(conn_info) + self.dynamic_provider_impls[provider_id] = impl + except Exception as e: + logger.error(f"Failed to instantiate provider {provider_id}: {e}") + # Update status to failed + conn_info.status = ProviderConnectionStatus.failed + conn_info.error_message = str(e) + conn_info.updated_at = datetime.now(UTC) + await self._store_connection(conn_info) async def shutdown(self) -> None: logger.debug("ProviderImpl.shutdown") @@ -174,13 +169,34 @@ class ProviderImpl(Providers): return ListProvidersResponse(data=ret) - async def inspect_provider(self, provider_id: str) -> ProviderInfo: + async def inspect_provider(self, provider_id: str) -> ListProvidersResponse: + """Get all providers with the given provider_id (deprecated). + + Returns all providers across all APIs that have this provider_id. + This is deprecated - use inspect_provider_for_api() for unambiguous access. + """ + all_providers = await self.list_providers() + matching = [p for p in all_providers.data if p.provider_id == provider_id] + + if not matching: + raise ValueError(f"Provider {provider_id} not found") + + return ListProvidersResponse(data=matching) + + async def list_providers_for_api(self, api: str) -> ListProvidersResponse: + """List providers for a specific API.""" + all_providers = await self.list_providers() + filtered = [p for p in all_providers.data if p.api == api] + return ListProvidersResponse(data=filtered) + + async def inspect_provider_for_api(self, api: str, provider_id: str) -> ProviderInfo: + """Get a specific provider for a specific API.""" all_providers = await self.list_providers() for p in all_providers.data: - if p.provider_id == provider_id: + if p.api == api and p.provider_id == provider_id: return p - raise ValueError(f"Provider {provider_id} not found") + raise ValueError(f"Provider {provider_id} not found for API {api}") async def get_providers_health(self) -> dict[str, dict[str, HealthResponse]]: """Get health status for all providers. @@ -272,17 +288,19 @@ class ProviderImpl(Providers): return ProviderConnectionInfo.model_validate_json(value) return None - async def _delete_connection(self, provider_id: str) -> None: + async def _delete_connection(self, api: str, provider_id: str) -> None: """Delete provider connection from kvstore. + :param api: API namespace :param provider_id: Provider ID to delete """ if not self.kvstore: raise RuntimeError("KVStore not initialized") - key = f"{PROVIDER_CONNECTIONS_PREFIX}{provider_id}" + # Use composite key: provider_connections:v1::{api}::{provider_id} + key = f"{PROVIDER_CONNECTIONS_PREFIX}{api}::{provider_id}" await self.kvstore.delete(key) - logger.debug(f"Deleted provider connection: {provider_id}") + logger.debug(f"Deleted provider connection: {api}::{provider_id}") async def _list_connections(self) -> list[ProviderConnectionInfo]: """List all dynamic provider connections from kvstore. @@ -306,6 +324,17 @@ class ProviderImpl(Providers): self.dynamic_providers[cache_key] = conn logger.debug(f"Loaded dynamic provider: {cache_key} (status: {conn.status})") + def _find_provider_cache_key(self, provider_id: str) -> str | None: + """Find the cache key for a provider by its provider_id. + + Since we use composite keys ({api}::{provider_id}), this searches for the matching key. + Returns None if not found. + """ + for key in self.dynamic_providers.keys(): + if key.endswith(f"::{provider_id}"): + return key + return None + # Helper methods for dynamic provider management def _redact_sensitive_config(self, config: dict[str, Any]) -> dict[str, Any]: @@ -380,8 +409,8 @@ class ProviderImpl(Providers): async def register_provider( self, - provider_id: str, api: str, + provider_id: str, provider_type: str, config: dict[str, Any], attributes: dict[str, list[str]] | None = None, @@ -394,7 +423,6 @@ class ProviderImpl(Providers): All providers are stored in kvstore and treated equally. """ - logger.info(f"📝 REGISTER_PROVIDER called: provider_id={provider_id}, api={api}, type={provider_type}") if not self.kvstore: raise RuntimeError("Dynamic provider management is not enabled (no kvstore configured)") @@ -427,25 +455,15 @@ class ProviderImpl(Providers): # Store in kvstore await self._store_connection(conn_info) - # Instantiate provider if we have a provider registry - if self.provider_registry: - impl = await self._instantiate_provider(conn_info) - # Use composite key for impl cache too - self.dynamic_provider_impls[cache_key] = impl + impl = await self._instantiate_provider(conn_info) + # Use composite key for impl cache too + self.dynamic_provider_impls[cache_key] = impl - # Update status to connected after successful instantiation - conn_info.status = ProviderConnectionStatus.connected - conn_info.updated_at = datetime.now(UTC) + # Update status to connected after successful instantiation + conn_info.status = ProviderConnectionStatus.connected + conn_info.updated_at = datetime.now(UTC) - logger.info( - f"Registered and instantiated dynamic provider {provider_id} (api={api}, type={provider_type})" - ) - else: - # No registry available - just mark as connected without instantiation - # This can happen during testing or if provider management is disabled - conn_info.status = ProviderConnectionStatus.connected - conn_info.updated_at = datetime.now(UTC) - logger.warning(f"Registered provider {provider_id} without instantiation (no registry)") + logger.info(f"Registered and instantiated dynamic provider {provider_id} (api={api}, type={provider_type})") # Store updated status await self._store_connection(conn_info) @@ -468,6 +486,7 @@ class ProviderImpl(Providers): async def update_provider( self, + api: str, provider_id: str, config: dict[str, Any] | None = None, attributes: dict[str, list[str]] | None = None, @@ -477,16 +496,16 @@ class ProviderImpl(Providers): Updates persist to kvstore and survive server restarts. This works for all providers (whether originally from run.yaml or API). """ - logger.info(f"🔄 UPDATE_PROVIDER called: provider_id={provider_id}, has_config={config is not None}, has_attributes={attributes is not None}") if not self.kvstore: raise RuntimeError("Dynamic provider management is not enabled (no kvstore configured)") - # Check if provider exists - if provider_id not in self.dynamic_providers: - raise ValueError(f"Provider {provider_id} not found") + # Use composite key + cache_key = f"{api}::{provider_id}" + if cache_key not in self.dynamic_providers: + raise ValueError(f"Provider {provider_id} not found for API {api}") - conn_info = self.dynamic_providers[provider_id] + conn_info = self.dynamic_providers[cache_key] # Update config if provided if config is not None: @@ -504,33 +523,26 @@ class ProviderImpl(Providers): await self._store_connection(conn_info) # Hot-reload: Shutdown old instance and reinstantiate with new config - if self.provider_registry: - # Shutdown old instance if it exists - if provider_id in self.dynamic_provider_impls: - old_impl = self.dynamic_provider_impls[provider_id] - if hasattr(old_impl, "shutdown"): - try: - await old_impl.shutdown() - logger.debug(f"Shutdown old instance of provider {provider_id}") - except Exception as e: - logger.warning(f"Error shutting down old instance of {provider_id}: {e}") + # Shutdown old instance if it exists + if cache_key in self.dynamic_provider_impls: + old_impl = self.dynamic_provider_impls[cache_key] + if hasattr(old_impl, "shutdown"): + try: + await old_impl.shutdown() + logger.debug(f"Shutdown old instance of provider {provider_id}") + except Exception as e: + logger.warning(f"Error shutting down old instance of {provider_id}: {e}") - # Reinstantiate with new config - impl = await self._instantiate_provider(conn_info) - self.dynamic_provider_impls[provider_id] = impl + # Reinstantiate with new config + impl = await self._instantiate_provider(conn_info) + self.dynamic_provider_impls[cache_key] = impl - # Update status to connected after successful reinstantiation - conn_info.status = ProviderConnectionStatus.connected - conn_info.updated_at = datetime.now(UTC) - await self._store_connection(conn_info) + # Update status to connected after successful reinstantiation + conn_info.status = ProviderConnectionStatus.connected + conn_info.updated_at = datetime.now(UTC) + await self._store_connection(conn_info) - logger.info(f"Hot-reloaded dynamic provider {provider_id}") - else: - # No registry - just update config without reinstantiation - conn_info.status = ProviderConnectionStatus.connected - conn_info.updated_at = datetime.now(UTC) - await self._store_connection(conn_info) - logger.warning(f"Updated provider {provider_id} config without hot-reload (no registry)") + logger.info(f"Hot-reloaded dynamic provider {provider_id}") return UpdateProviderResponse(provider=conn_info) @@ -543,34 +555,36 @@ class ProviderImpl(Providers): logger.error(f"Failed to update provider {provider_id}: {e}") raise RuntimeError(f"Failed to update provider: {e}") from e - async def unregister_provider(self, provider_id: str) -> None: + async def unregister_provider(self, api: str, provider_id: str) -> None: """Unregister a provider. Removes the provider from kvstore and shuts down its instance. This works for all providers (whether originally from run.yaml or API). """ - logger.info(f"🗑️ UNREGISTER_PROVIDER called: provider_id={provider_id}") if not self.kvstore: raise RuntimeError("Dynamic provider management is not enabled (no kvstore configured)") - # Check if provider exists - if provider_id not in self.dynamic_providers: - raise ValueError(f"Provider {provider_id} not found") + # Use composite key + cache_key = f"{api}::{provider_id}" + if cache_key not in self.dynamic_providers: + raise ValueError(f"Provider {provider_id} not found for API {api}") + + conn_info = self.dynamic_providers[cache_key] try: # Shutdown provider instance if it exists - if provider_id in self.dynamic_provider_impls: - impl = self.dynamic_provider_impls[provider_id] + if cache_key in self.dynamic_provider_impls: + impl = self.dynamic_provider_impls[cache_key] if hasattr(impl, "shutdown"): await impl.shutdown() - del self.dynamic_provider_impls[provider_id] + del self.dynamic_provider_impls[cache_key] - # Remove from kvstore - await self._delete_connection(provider_id) + # Remove from kvstore (using the api and provider_id from conn_info) + await self._delete_connection(conn_info.api, provider_id) # Remove from runtime cache - del self.dynamic_providers[provider_id] + del self.dynamic_providers[cache_key] logger.info(f"Unregistered dynamic provider {provider_id}") @@ -578,23 +592,24 @@ class ProviderImpl(Providers): logger.error(f"Failed to unregister provider {provider_id}: {e}") raise RuntimeError(f"Failed to unregister provider: {e}") from e - async def test_provider_connection(self, provider_id: str) -> TestProviderConnectionResponse: + async def test_provider_connection(self, api: str, provider_id: str) -> TestProviderConnectionResponse: """Test a provider connection.""" - logger.info(f"🔍 TEST_PROVIDER_CONNECTION called: provider_id={provider_id}") # Check if provider exists (static or dynamic) provider_impl = None + cache_key = f"{api}::{provider_id}" + + # Check dynamic providers first (using composite keys) + if cache_key in self.dynamic_provider_impls: + provider_impl = self.dynamic_provider_impls[cache_key] - # Check dynamic providers first - if provider_id in self.dynamic_provider_impls: - provider_impl = self.dynamic_provider_impls[provider_id] # Check static providers - elif provider_id in self.deps: + if not provider_impl and provider_id in self.deps: provider_impl = self.deps[provider_id] if not provider_impl: return TestProviderConnectionResponse( - success=False, error_message=f"Provider {provider_id} not found or not initialized" + success=False, error_message=f"Provider {provider_id} not found for API {api}" ) # Check if provider has health method @@ -611,8 +626,8 @@ class ProviderImpl(Providers): health_result = await asyncio.wait_for(provider_impl.health(), timeout=5.0) # Update health in dynamic provider cache if applicable - if provider_id in self.dynamic_providers: - conn_info = self.dynamic_providers[provider_id] + if cache_key and cache_key in self.dynamic_providers: + conn_info = self.dynamic_providers[cache_key] conn_info.health = ProviderHealth.from_health_response(health_result) conn_info.last_health_check = datetime.now(UTC) await self._store_connection(conn_info) diff --git a/llama_stack/core/stack.py b/llama_stack/core/stack.py index 6fadefeb3..4b745bec5 100644 --- a/llama_stack/core/stack.py +++ b/llama_stack/core/stack.py @@ -34,16 +34,21 @@ from llama_stack.apis.synthetic_data_generation import SyntheticDataGeneration from llama_stack.apis.telemetry import Telemetry from llama_stack.apis.tools import RAGToolRuntime, ToolGroups, ToolRuntime from llama_stack.apis.vector_io import VectorIO +from llama_stack.core.access_control.datatypes import AccessRule from llama_stack.core.conversations.conversations import ConversationServiceConfig, ConversationServiceImpl from llama_stack.core.datatypes import Provider, SafetyConfig, StackRunConfig, VectorStoresConfig -from llama_stack.core.distribution import get_provider_registry +from llama_stack.core.distribution import builtin_automatically_routed_apis, get_provider_registry from llama_stack.core.inspect import DistributionInspectConfig, DistributionInspectImpl from llama_stack.core.prompts.prompts import PromptServiceConfig, PromptServiceImpl from llama_stack.core.providers import ProviderImpl, ProviderImplConfig -from llama_stack.core.resolver import ProviderRegistry, resolve_impls +from llama_stack.core.resolver import ( + ProviderRegistry, + instantiate_provider, + sort_providers_by_deps, + specs_for_autorouted_apis, + validate_and_prepare_providers, +) from llama_stack.core.routing_tables.common import CommonRoutingTableImpl -from llama_stack.core.access_control.datatypes import AccessRule -from llama_stack.core.store.registry import DistributionRegistry from llama_stack.core.storage.datatypes import ( InferenceStoreReference, KVStoreReference, @@ -54,10 +59,12 @@ from llama_stack.core.storage.datatypes import ( StorageBackendConfig, StorageConfig, ) -from llama_stack.core.store.registry import create_dist_registry +from llama_stack.core.store.registry import DistributionRegistry, create_dist_registry from llama_stack.core.utils.dynamic import instantiate_class_type from llama_stack.log import get_logger from llama_stack.providers.datatypes import Api +from llama_stack.providers.utils.kvstore.kvstore import register_kvstore_backends +from llama_stack.providers.utils.sqlstore.sqlstore import register_sqlstore_backends logger = get_logger(name=__name__, category="core") @@ -401,9 +408,6 @@ def _initialize_storage(run_config: StackRunConfig): else: raise ValueError(f"Unknown storage backend type: {type}") - from llama_stack.providers.utils.kvstore.kvstore import register_kvstore_backends - from llama_stack.providers.utils.sqlstore.sqlstore import register_sqlstore_backends - register_kvstore_backends(kv_backends) register_sqlstore_backends(sql_backends) @@ -429,9 +433,6 @@ async def resolve_impls_via_provider_registration( Returns: Dictionary mapping API to implementation instances """ - from llama_stack.core.distribution import builtin_automatically_routed_apis - from llama_stack.core.resolver import sort_providers_by_deps, specs_for_autorouted_apis, validate_and_prepare_providers - routing_table_apis = {x.routing_table_api for x in builtin_automatically_routed_apis()} router_apis = {x.router_api for x in builtin_automatically_routed_apis()} @@ -455,50 +456,29 @@ async def resolve_impls_via_provider_registration( # Register each provider through ProviderImpl impls = internal_impls.copy() - logger.info(f"🚀 Starting provider registration for {len(sorted_providers)} providers from run.yaml") + logger.info(f"Provider registration for {len(sorted_providers)} providers from run.yaml") for api_str, provider in sorted_providers: # Skip providers that are not enabled if provider.provider_id is None: continue - # Skip internal APIs that need special handling - # - providers: already initialized as internal_impls - # - inspect: already initialized as internal_impls - # - telemetry: internal observability, directly instantiated below + # Skip internal APIs (already initialized) if api_str in ["providers", "inspect"]: continue - # Telemetry is an internal API that should be directly instantiated - if api_str == "telemetry": - logger.info(f"Instantiating {provider.provider_id} for {api_str}") - - from llama_stack.core.resolver import instantiate_provider - - deps = {a: impls[a] for a in provider.spec.api_dependencies if a in impls} - for a in provider.spec.optional_api_dependencies: - if a in impls: - deps[a] = impls[a] - - impl = await instantiate_provider(provider, deps, {}, dist_registry, run_config, policy) - api = Api(api_str) - impls[api] = impl - providers_impl.deps[api] = impl - continue - # Handle different provider types try: - # Check if this is a routing table or router (system infrastructure) - is_routing_table = api_str.startswith("inner-") or provider.spec.provider_type in ["routing_table", "router"] - is_router = not api_str.startswith("inner-") and (Api(api_str) in router_apis or provider.spec.provider_type == "router") + # Check if this is a router (system infrastructure) + is_router = not api_str.startswith("inner-") and ( + Api(api_str) in router_apis or provider.spec.provider_type == "router" + ) if api_str.startswith("inner-") or provider.spec.provider_type == "routing_table": # Inner providers or routing tables cannot be registered through the API # They need to be instantiated directly logger.info(f"Instantiating {provider.provider_id} for {api_str}") - from llama_stack.core.resolver import instantiate_provider - deps = {a: impls[a] for a in provider.spec.api_dependencies if a in impls} for a in provider.spec.optional_api_dependencies: if a in impls: @@ -510,8 +490,9 @@ async def resolve_impls_via_provider_registration( # For routing tables of autorouted APIs, get inner impls from the router API # E.g., tool_groups routing table needs inner-tool_runtime providers if provider.spec.provider_type == "routing_table": - from llama_stack.core.distribution import builtin_automatically_routed_apis - autorouted_map = {info.routing_table_api: info.router_api for info in builtin_automatically_routed_apis()} + autorouted_map = { + info.routing_table_api: info.router_api for info in builtin_automatically_routed_apis() + } if Api(api_str) in autorouted_map: router_api_str = autorouted_map[Api(api_str)].value inner_key = f"inner-{router_api_str}" @@ -540,8 +521,6 @@ async def resolve_impls_via_provider_registration( # Router providers also need special handling logger.info(f"Instantiating router {provider.provider_id} for {api_str}") - from llama_stack.core.resolver import instantiate_provider - deps = {a: impls[a] for a in provider.spec.api_dependencies if a in impls} for a in provider.spec.optional_api_dependencies: if a in impls: @@ -564,9 +543,9 @@ async def resolve_impls_via_provider_registration( api = Api(api_str) logger.info(f"Registering {provider.provider_id} for {api.value}") - response = await providers_impl.register_provider( - provider_id=provider.provider_id, + await providers_impl.register_provider( api=api.value, + provider_id=provider.provider_id, provider_type=provider.spec.provider_type, config=provider.config, attributes=getattr(provider, "attributes", None), @@ -580,10 +559,10 @@ async def resolve_impls_via_provider_registration( # IMPORTANT: Update providers_impl.deps so subsequent providers can depend on this one providers_impl.deps[api] = impl - logger.info(f"✅ Successfully registered startup provider: {provider.provider_id}") + logger.info(f"Successfully registered startup provider: {provider.provider_id}") except Exception as e: - logger.error(f"❌ Failed to handle provider {provider.provider_id}: {e}") + logger.error(f"Failed to handle provider {provider.provider_id}: {e}") raise return impls diff --git a/llama_stack/distributions/ci-tests/run.yaml b/llama_stack/distributions/ci-tests/run.yaml index 6938dbb92..ed880d4a0 100644 --- a/llama_stack/distributions/ci-tests/run.yaml +++ b/llama_stack/distributions/ci-tests/run.yaml @@ -231,7 +231,7 @@ storage: backends: kv_default: type: kv_sqlite - db_path: ":memory:" + db_path: ${env.SQLITE_STORE_DIR:=~/.llama/distributions/ci-tests}/kvstore.db sql_default: type: sql_sqlite db_path: ${env.SQLITE_STORE_DIR:=~/.llama/distributions/ci-tests}/sql_store.db diff --git a/tests/unit/core/test_dynamic_providers.py b/tests/unit/core/test_dynamic_providers.py index 8992b67b7..c7f3c5f05 100644 --- a/tests/unit/core/test_dynamic_providers.py +++ b/tests/unit/core/test_dynamic_providers.py @@ -83,8 +83,8 @@ class TestDynamicProviderManagement: with patch.object(provider_impl, "_instantiate_provider", return_value=mock_provider_instance): # Register a mock inference provider response = await provider_impl.register_provider( - provider_id="test-inference-1", api=Api.inference.value, + provider_id="test-inference-1", provider_type="remote::openai", config={"api_key": "test-key", "url": "https://api.openai.com/v1"}, attributes={"team": ["test-team"]}, @@ -98,9 +98,9 @@ class TestDynamicProviderManagement: assert response.provider.config["api_key"] == "test-key" assert response.provider.attributes == {"team": ["test-team"]} - # Verify provider is stored - assert "test-inference-1" in provider_impl.dynamic_providers - assert "test-inference-1" in provider_impl.dynamic_provider_impls + # Verify provider is stored (using composite key) + assert "inference::test-inference-1" in provider_impl.dynamic_providers + assert "inference::test-inference-1" in provider_impl.dynamic_provider_impls async def test_register_vector_store_provider(self, provider_impl): """Test registering a new vector store provider.""" @@ -111,8 +111,8 @@ class TestDynamicProviderManagement: with patch.object(provider_impl, "_instantiate_provider", return_value=mock_provider_instance): # Register a mock vector_io provider response = await provider_impl.register_provider( - provider_id="test-vector-store-1", api=Api.vector_io.value, + provider_id="test-vector-store-1", provider_type="inline::faiss", config={"dimension": 768, "index_path": "/tmp/faiss_index"}, ) @@ -132,8 +132,8 @@ class TestDynamicProviderManagement: with patch.object(provider_impl, "_instantiate_provider", return_value=mock_provider_instance): # Register first provider await provider_impl.register_provider( - provider_id="test-duplicate", api=Api.inference.value, + provider_id="test-duplicate", provider_type="remote::openai", config={"api_key": "key1"}, ) @@ -141,8 +141,8 @@ class TestDynamicProviderManagement: # Try to register with same ID with pytest.raises(ValueError, match="already exists"): await provider_impl.register_provider( - provider_id="test-duplicate", api=Api.inference.value, + provider_id="test-duplicate", provider_type="remote::openai", config={"api_key": "key2"}, ) @@ -155,14 +155,15 @@ class TestDynamicProviderManagement: with patch.object(provider_impl, "_instantiate_provider", return_value=mock_provider_instance): # Register provider await provider_impl.register_provider( - provider_id="test-update", api=Api.inference.value, + provider_id="test-update", provider_type="remote::openai", config={"api_key": "old-key", "timeout": 30}, ) # Update configuration response = await provider_impl.update_provider( + api=Api.inference.value, provider_id="test-update", config={"api_key": "new-key", "timeout": 60}, ) @@ -181,8 +182,8 @@ class TestDynamicProviderManagement: with patch.object(provider_impl, "_instantiate_provider", return_value=mock_provider_instance): # Register provider with initial attributes await provider_impl.register_provider( - provider_id="test-attributes", api=Api.inference.value, + provider_id="test-attributes", provider_type="remote::openai", config={"api_key": "test-key"}, attributes={"team": ["team-a"]}, @@ -190,6 +191,7 @@ class TestDynamicProviderManagement: # Update attributes response = await provider_impl.update_provider( + api=Api.inference.value, provider_id="test-attributes", attributes={"team": ["team-a", "team-b"], "environment": ["prod"]}, ) @@ -201,6 +203,7 @@ class TestDynamicProviderManagement: """Test that updating a non-existent provider fails.""" with pytest.raises(ValueError, match="not found"): await provider_impl.update_provider( + api=Api.inference.value, provider_id="nonexistent", config={"api_key": "new-key"}, ) @@ -214,21 +217,22 @@ class TestDynamicProviderManagement: with patch.object(provider_impl, "_instantiate_provider", return_value=mock_provider_instance): # Register provider await provider_impl.register_provider( - provider_id="test-unregister", api=Api.inference.value, + provider_id="test-unregister", provider_type="remote::openai", config={"api_key": "test-key"}, ) # Verify it exists - assert "test-unregister" in provider_impl.dynamic_providers + cache_key = f"{Api.inference.value}::test-unregister" + assert cache_key in provider_impl.dynamic_providers # Unregister provider - await provider_impl.unregister_provider(provider_id="test-unregister") + await provider_impl.unregister_provider(api=Api.inference.value, provider_id="test-unregister") # Verify it's removed - assert "test-unregister" not in provider_impl.dynamic_providers - assert "test-unregister" not in provider_impl.dynamic_provider_impls + assert cache_key not in provider_impl.dynamic_providers + assert cache_key not in provider_impl.dynamic_provider_impls # Verify shutdown was called mock_provider_instance.shutdown.assert_called_once() @@ -236,7 +240,7 @@ class TestDynamicProviderManagement: async def test_unregister_nonexistent_provider_fails(self, provider_impl): """Test that unregistering a non-existent provider fails.""" with pytest.raises(ValueError, match="not found"): - await provider_impl.unregister_provider(provider_id="nonexistent") + await provider_impl.unregister_provider(api=Api.inference.value, provider_id="nonexistent") async def test_test_provider_connection_healthy(self, provider_impl): """Test testing a healthy provider connection.""" @@ -246,14 +250,14 @@ class TestDynamicProviderManagement: with patch.object(provider_impl, "_instantiate_provider", return_value=mock_provider_instance): # Register provider await provider_impl.register_provider( - provider_id="test-health", api=Api.inference.value, + provider_id="test-health", provider_type="remote::openai", config={"api_key": "test-key"}, ) # Test connection - response = await provider_impl.test_provider_connection(provider_id="test-health") + response = await provider_impl.test_provider_connection(api=Api.inference.value, provider_id="test-health") # Verify response assert response.success is True @@ -271,14 +275,16 @@ class TestDynamicProviderManagement: with patch.object(provider_impl, "_instantiate_provider", return_value=mock_provider_instance): # Register provider await provider_impl.register_provider( - provider_id="test-unhealthy", api=Api.inference.value, + provider_id="test-unhealthy", provider_type="remote::openai", config={"api_key": "invalid-key"}, ) # Test connection - response = await provider_impl.test_provider_connection(provider_id="test-unhealthy") + response = await provider_impl.test_provider_connection( + api=Api.inference.value, provider_id="test-unhealthy" + ) # Verify response shows unhealthy status assert response.success is False @@ -292,15 +298,15 @@ class TestDynamicProviderManagement: with patch.object(provider_impl, "_instantiate_provider", return_value=mock_provider_instance): # Register multiple providers await provider_impl.register_provider( - provider_id="dynamic-1", api=Api.inference.value, + provider_id="dynamic-1", provider_type="remote::openai", config={"api_key": "key1"}, ) await provider_impl.register_provider( - provider_id="dynamic-2", api=Api.vector_io.value, + provider_id="dynamic-2", provider_type="inline::faiss", config={"dimension": 768}, ) @@ -321,8 +327,8 @@ class TestDynamicProviderManagement: with patch.object(provider_impl, "_instantiate_provider", return_value=mock_provider_instance): # Register provider await provider_impl.register_provider( - provider_id="test-inspect", api=Api.inference.value, + provider_id="test-inspect", provider_type="remote::openai", config={"api_key": "test-key", "model": "gpt-4"}, ) @@ -330,14 +336,17 @@ class TestDynamicProviderManagement: # Update the stored health info to reflect OK status # (In reality, the health check happens during registration, # but our mock may not have been properly called) - conn_info = provider_impl.dynamic_providers["test-inspect"] + cache_key = f"{Api.inference.value}::test-inspect" + conn_info = provider_impl.dynamic_providers[cache_key] conn_info.health = ProviderHealth.from_health_response({"status": HealthStatus.OK}) # Inspect provider - provider_info = await provider_impl.inspect_provider(provider_id="test-inspect") + response = await provider_impl.inspect_provider(provider_id="test-inspect") - # Verify provider info + # Verify response + assert len(response.data) == 1 + provider_info = response.data[0] assert provider_info.provider_id == "test-inspect" assert provider_info.api == Api.inference.value assert provider_info.provider_type == "remote::openai" @@ -352,8 +361,8 @@ class TestDynamicProviderManagement: with patch.object(provider_impl, "_instantiate_provider", return_value=mock_provider_instance): # Register provider await provider_impl.register_provider( - provider_id="test-persist", api=Api.inference.value, + provider_id="test-persist", provider_type="remote::openai", config={"api_key": "persist-key"}, ) @@ -397,5 +406,6 @@ class TestDynamicProviderManagement: await new_impl._load_dynamic_providers() # Verify the provider was loaded from kvstore - assert "test-persist" in new_impl.dynamic_providers - assert new_impl.dynamic_providers["test-persist"].config["api_key"] == "persist-key" + cache_key = f"{Api.inference.value}::test-persist" + assert cache_key in new_impl.dynamic_providers + assert new_impl.dynamic_providers[cache_key].config["api_key"] == "persist-key"