diff --git a/client-sdks/stainless/openapi.yml b/client-sdks/stainless/openapi.yml index 7b03cd03e..2953055a5 100644 --- a/client-sdks/stainless/openapi.yml +++ b/client-sdks/stainless/openapi.yml @@ -15,6 +15,120 @@ info: servers: - url: http://any-hosted-llama-stack.com paths: + /v1/admin/providers: + post: + responses: + '200': + description: >- + RegisterProviderResponse with the registered provider info. + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterProviderResponse' + '400': + $ref: '#/components/responses/BadRequest400' + '429': + $ref: >- + #/components/responses/TooManyRequests429 + '500': + $ref: >- + #/components/responses/InternalServerError500 + default: + $ref: '#/components/responses/DefaultError' + tags: + - Providers + summary: Register a new dynamic provider. + description: >- + Register a new dynamic provider. + + Register a new provider instance at runtime. The provider will be validated, + + instantiated, and persisted to the kvstore. Requires appropriate ABAC permissions. + parameters: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterProviderRequest' + required: true + deprecated: false + /v1/admin/providers/{provider_id}: + post: + responses: + '200': + description: >- + UpdateProviderResponse with updated provider info + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateProviderResponse' + '400': + $ref: '#/components/responses/BadRequest400' + '429': + $ref: >- + #/components/responses/TooManyRequests429 + '500': + $ref: >- + #/components/responses/InternalServerError500 + default: + $ref: '#/components/responses/DefaultError' + tags: + - Providers + summary: >- + Update an existing provider's configuration. + description: >- + 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. + parameters: + - name: provider_id + in: path + description: ID of the provider to update + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateProviderRequest' + required: true + deprecated: false + delete: + responses: + '200': + description: OK + '400': + $ref: '#/components/responses/BadRequest400' + '429': + $ref: >- + #/components/responses/TooManyRequests429 + '500': + $ref: >- + #/components/responses/InternalServerError500 + default: + $ref: '#/components/responses/DefaultError' + tags: + - Providers + summary: Unregister a dynamic provider. + description: >- + 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. + parameters: + - name: provider_id + in: path + description: ID of the provider to unregister. + required: true + schema: + type: string + deprecated: false /v1/chat/completions: get: responses: @@ -1289,6 +1403,43 @@ paths: 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. + required: true + schema: + type: string + deprecated: false /v1/responses: get: responses: @@ -4212,6 +4363,251 @@ components: title: Error description: >- Error response from the API. Roughly follows RFC 7807. + RegisterProviderRequest: + type: object + properties: + provider_id: + 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'). + config: + type: object + additionalProperties: + oneOf: + - type: 'null' + - type: boolean + - type: number + - type: string + - type: array + - type: object + description: >- + Provider configuration (API keys, endpoints, etc.). + attributes: + type: object + additionalProperties: + type: array + items: + type: string + description: >- + Optional attributes for ABAC access control. + additionalProperties: false + required: + - provider_id + - api + - provider_type + - config + title: RegisterProviderRequest + ProviderConnectionInfo: + type: object + properties: + provider_id: + type: string + description: >- + Unique identifier for this provider instance + api: + type: string + description: >- + API namespace (e.g., "inference", "vector_io", "safety") + provider_type: + type: string + description: >- + Provider type identifier (e.g., "remote::openai", "inline::faiss") + config: + type: object + additionalProperties: + oneOf: + - type: 'null' + - type: boolean + - type: number + - type: string + - type: array + - type: object + description: >- + Provider-specific configuration (API keys, endpoints, etc.) + status: + $ref: '#/components/schemas/ProviderConnectionStatus' + description: Current connection status + health: + $ref: '#/components/schemas/ProviderHealth' + description: Most recent health check result + created_at: + type: string + format: date-time + description: Timestamp when provider was registered + updated_at: + type: string + format: date-time + description: Timestamp of last update + last_health_check: + type: string + format: date-time + description: Timestamp of last health check + error_message: + type: string + description: Error message if status is failed + metadata: + type: object + additionalProperties: + oneOf: + - type: 'null' + - type: boolean + - type: number + - type: string + - type: array + - type: object + description: >- + User-defined metadata (deprecated, use attributes) + owner: + type: object + properties: + principal: + type: string + attributes: + type: object + additionalProperties: + type: array + items: + type: string + additionalProperties: false + required: + - principal + description: >- + User who created this provider connection + attributes: + type: object + additionalProperties: + type: array + items: + type: string + description: >- + Key-value attributes for ABAC access control + additionalProperties: false + required: + - provider_id + - api + - provider_type + - config + - status + - created_at + - updated_at + - metadata + title: ProviderConnectionInfo + description: >- + Information about a dynamically managed provider connection. + + This model represents a provider that has been registered at runtime + + via the /providers API, as opposed to static providers configured in run.yaml. + + + Dynamic providers support full lifecycle management including registration, + + configuration updates, health monitoring, and removal. + ProviderConnectionStatus: + type: string + enum: + - pending + - initializing + - connected + - failed + - disconnected + - testing + title: ProviderConnectionStatus + description: Status of a dynamic provider connection. + ProviderHealth: + type: object + properties: + status: + type: string + enum: + - OK + - Error + - Not Implemented + description: >- + Health status (OK, ERROR, NOT_IMPLEMENTED) + message: + type: string + description: Optional error or status message + metrics: + type: object + additionalProperties: + oneOf: + - type: 'null' + - type: boolean + - type: number + - type: string + - type: array + - type: object + description: Provider-specific health metrics + last_checked: + type: string + format: date-time + description: Timestamp of last health check + additionalProperties: false + required: + - status + - metrics + - last_checked + title: ProviderHealth + description: >- + Structured wrapper around provider health status. + + This wraps the existing dict-based HealthResponse for API responses + + while maintaining backward compatibility with existing provider implementations. + RegisterProviderResponse: + type: object + properties: + provider: + $ref: '#/components/schemas/ProviderConnectionInfo' + description: >- + Information about the registered provider + additionalProperties: false + required: + - provider + title: RegisterProviderResponse + description: Response after registering a provider. + UpdateProviderRequest: + type: object + properties: + config: + type: object + additionalProperties: + oneOf: + - type: 'null' + - type: boolean + - type: number + - type: string + - type: array + - type: object + description: >- + New configuration parameters (merged with existing) + attributes: + type: object + additionalProperties: + type: array + items: + type: string + description: New attributes for access control + additionalProperties: false + title: UpdateProviderRequest + UpdateProviderResponse: + type: object + properties: + provider: + $ref: '#/components/schemas/ProviderConnectionInfo' + description: Updated provider information + additionalProperties: false + required: + - provider + title: UpdateProviderResponse + description: Response after updating a provider. Order: type: string enum: @@ -6680,6 +7076,32 @@ 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/llama-stack-spec.html b/docs/static/llama-stack-spec.html index 5d8b62db3..8df813176 100644 --- a/docs/static/llama-stack-spec.html +++ b/docs/static/llama-stack-spec.html @@ -40,6 +40,142 @@ } ], "paths": { + "/v1/admin/providers": { + "post": { + "responses": { + "200": { + "description": "RegisterProviderResponse with the registered provider info.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterProviderResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest400" + }, + "429": { + "$ref": "#/components/responses/TooManyRequests429" + }, + "500": { + "$ref": "#/components/responses/InternalServerError500" + }, + "default": { + "$ref": "#/components/responses/DefaultError" + } + }, + "tags": [ + "Providers" + ], + "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": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterProviderRequest" + } + } + }, + "required": true + }, + "deprecated": false + } + }, + "/v1/admin/providers/{provider_id}": { + "post": { + "responses": { + "200": { + "description": "UpdateProviderResponse with updated provider info", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateProviderResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest400" + }, + "429": { + "$ref": "#/components/responses/TooManyRequests429" + }, + "500": { + "$ref": "#/components/responses/InternalServerError500" + }, + "default": { + "$ref": "#/components/responses/DefaultError" + } + }, + "tags": [ + "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.", + "parameters": [ + { + "name": "provider_id", + "in": "path", + "description": "ID of the provider to update", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateProviderRequest" + } + } + }, + "required": true + }, + "deprecated": false + }, + "delete": { + "responses": { + "200": { + "description": "OK" + }, + "400": { + "$ref": "#/components/responses/BadRequest400" + }, + "429": { + "$ref": "#/components/responses/TooManyRequests429" + }, + "500": { + "$ref": "#/components/responses/InternalServerError500" + }, + "default": { + "$ref": "#/components/responses/DefaultError" + } + }, + "tags": [ + "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.", + "parameters": [ + { + "name": "provider_id", + "in": "path", + "description": "ID of the provider to unregister.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "deprecated": false + } + }, "/v1/chat/completions": { "get": { "responses": { @@ -1680,6 +1816,51 @@ "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.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "deprecated": false + } + }, "/v1/responses": { "get": { "responses": { @@ -4005,6 +4186,351 @@ "title": "Error", "description": "Error response from the API. Roughly follows RFC 7807." }, + "RegisterProviderRequest": { + "type": "object", + "properties": { + "provider_id": { + "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')." + }, + "config": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "array" + }, + { + "type": "object" + } + ] + }, + "description": "Provider configuration (API keys, endpoints, etc.)." + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Optional attributes for ABAC access control." + } + }, + "additionalProperties": false, + "required": [ + "provider_id", + "api", + "provider_type", + "config" + ], + "title": "RegisterProviderRequest" + }, + "ProviderConnectionInfo": { + "type": "object", + "properties": { + "provider_id": { + "type": "string", + "description": "Unique identifier for this provider instance" + }, + "api": { + "type": "string", + "description": "API namespace (e.g., \"inference\", \"vector_io\", \"safety\")" + }, + "provider_type": { + "type": "string", + "description": "Provider type identifier (e.g., \"remote::openai\", \"inline::faiss\")" + }, + "config": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "array" + }, + { + "type": "object" + } + ] + }, + "description": "Provider-specific configuration (API keys, endpoints, etc.)" + }, + "status": { + "$ref": "#/components/schemas/ProviderConnectionStatus", + "description": "Current connection status" + }, + "health": { + "$ref": "#/components/schemas/ProviderHealth", + "description": "Most recent health check result" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when provider was registered" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of last update" + }, + "last_health_check": { + "type": "string", + "format": "date-time", + "description": "Timestamp of last health check" + }, + "error_message": { + "type": "string", + "description": "Error message if status is failed" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "array" + }, + { + "type": "object" + } + ] + }, + "description": "User-defined metadata (deprecated, use attributes)" + }, + "owner": { + "type": "object", + "properties": { + "principal": { + "type": "string" + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "additionalProperties": false, + "required": [ + "principal" + ], + "description": "User who created this provider connection" + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Key-value attributes for ABAC access control" + } + }, + "additionalProperties": false, + "required": [ + "provider_id", + "api", + "provider_type", + "config", + "status", + "created_at", + "updated_at", + "metadata" + ], + "title": "ProviderConnectionInfo", + "description": "Information about a dynamically managed provider connection.\nThis model represents a provider that has been registered at runtime\nvia the /providers API, as opposed to static providers configured in run.yaml.\n\nDynamic providers support full lifecycle management including registration,\nconfiguration updates, health monitoring, and removal." + }, + "ProviderConnectionStatus": { + "type": "string", + "enum": [ + "pending", + "initializing", + "connected", + "failed", + "disconnected", + "testing" + ], + "title": "ProviderConnectionStatus", + "description": "Status of a dynamic provider connection." + }, + "ProviderHealth": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "OK", + "Error", + "Not Implemented" + ], + "description": "Health status (OK, ERROR, NOT_IMPLEMENTED)" + }, + "message": { + "type": "string", + "description": "Optional error or status message" + }, + "metrics": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "array" + }, + { + "type": "object" + } + ] + }, + "description": "Provider-specific health metrics" + }, + "last_checked": { + "type": "string", + "format": "date-time", + "description": "Timestamp of last health check" + } + }, + "additionalProperties": false, + "required": [ + "status", + "metrics", + "last_checked" + ], + "title": "ProviderHealth", + "description": "Structured wrapper around provider health status.\nThis wraps the existing dict-based HealthResponse for API responses\nwhile maintaining backward compatibility with existing provider implementations." + }, + "RegisterProviderResponse": { + "type": "object", + "properties": { + "provider": { + "$ref": "#/components/schemas/ProviderConnectionInfo", + "description": "Information about the registered provider" + } + }, + "additionalProperties": false, + "required": [ + "provider" + ], + "title": "RegisterProviderResponse", + "description": "Response after registering a provider." + }, + "UpdateProviderRequest": { + "type": "object", + "properties": { + "config": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "array" + }, + { + "type": "object" + } + ] + }, + "description": "New configuration parameters (merged with existing)" + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "New attributes for access control" + } + }, + "additionalProperties": false, + "title": "UpdateProviderRequest" + }, + "UpdateProviderResponse": { + "type": "object", + "properties": { + "provider": { + "$ref": "#/components/schemas/ProviderConnectionInfo", + "description": "Updated provider information" + } + }, + "additionalProperties": false, + "required": [ + "provider" + ], + "title": "UpdateProviderResponse", + "description": "Response after updating a provider." + }, "Order": { "type": "string", "enum": [ @@ -7242,6 +7768,51 @@ "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 435520356..4eafa60e5 100644 --- a/docs/static/llama-stack-spec.yaml +++ b/docs/static/llama-stack-spec.yaml @@ -12,6 +12,120 @@ info: servers: - url: http://any-hosted-llama-stack.com paths: + /v1/admin/providers: + post: + responses: + '200': + description: >- + RegisterProviderResponse with the registered provider info. + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterProviderResponse' + '400': + $ref: '#/components/responses/BadRequest400' + '429': + $ref: >- + #/components/responses/TooManyRequests429 + '500': + $ref: >- + #/components/responses/InternalServerError500 + default: + $ref: '#/components/responses/DefaultError' + tags: + - Providers + summary: Register a new dynamic provider. + description: >- + Register a new dynamic provider. + + Register a new provider instance at runtime. The provider will be validated, + + instantiated, and persisted to the kvstore. Requires appropriate ABAC permissions. + parameters: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterProviderRequest' + required: true + deprecated: false + /v1/admin/providers/{provider_id}: + post: + responses: + '200': + description: >- + UpdateProviderResponse with updated provider info + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateProviderResponse' + '400': + $ref: '#/components/responses/BadRequest400' + '429': + $ref: >- + #/components/responses/TooManyRequests429 + '500': + $ref: >- + #/components/responses/InternalServerError500 + default: + $ref: '#/components/responses/DefaultError' + tags: + - Providers + summary: >- + Update an existing provider's configuration. + description: >- + 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. + parameters: + - name: provider_id + in: path + description: ID of the provider to update + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateProviderRequest' + required: true + deprecated: false + delete: + responses: + '200': + description: OK + '400': + $ref: '#/components/responses/BadRequest400' + '429': + $ref: >- + #/components/responses/TooManyRequests429 + '500': + $ref: >- + #/components/responses/InternalServerError500 + default: + $ref: '#/components/responses/DefaultError' + tags: + - Providers + summary: Unregister a dynamic provider. + description: >- + 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. + parameters: + - name: provider_id + in: path + description: ID of the provider to unregister. + required: true + schema: + type: string + deprecated: false /v1/chat/completions: get: responses: @@ -1286,6 +1400,43 @@ paths: 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. + required: true + schema: + type: string + deprecated: false /v1/responses: get: responses: @@ -2999,6 +3150,251 @@ components: title: Error description: >- Error response from the API. Roughly follows RFC 7807. + RegisterProviderRequest: + type: object + properties: + provider_id: + 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'). + config: + type: object + additionalProperties: + oneOf: + - type: 'null' + - type: boolean + - type: number + - type: string + - type: array + - type: object + description: >- + Provider configuration (API keys, endpoints, etc.). + attributes: + type: object + additionalProperties: + type: array + items: + type: string + description: >- + Optional attributes for ABAC access control. + additionalProperties: false + required: + - provider_id + - api + - provider_type + - config + title: RegisterProviderRequest + ProviderConnectionInfo: + type: object + properties: + provider_id: + type: string + description: >- + Unique identifier for this provider instance + api: + type: string + description: >- + API namespace (e.g., "inference", "vector_io", "safety") + provider_type: + type: string + description: >- + Provider type identifier (e.g., "remote::openai", "inline::faiss") + config: + type: object + additionalProperties: + oneOf: + - type: 'null' + - type: boolean + - type: number + - type: string + - type: array + - type: object + description: >- + Provider-specific configuration (API keys, endpoints, etc.) + status: + $ref: '#/components/schemas/ProviderConnectionStatus' + description: Current connection status + health: + $ref: '#/components/schemas/ProviderHealth' + description: Most recent health check result + created_at: + type: string + format: date-time + description: Timestamp when provider was registered + updated_at: + type: string + format: date-time + description: Timestamp of last update + last_health_check: + type: string + format: date-time + description: Timestamp of last health check + error_message: + type: string + description: Error message if status is failed + metadata: + type: object + additionalProperties: + oneOf: + - type: 'null' + - type: boolean + - type: number + - type: string + - type: array + - type: object + description: >- + User-defined metadata (deprecated, use attributes) + owner: + type: object + properties: + principal: + type: string + attributes: + type: object + additionalProperties: + type: array + items: + type: string + additionalProperties: false + required: + - principal + description: >- + User who created this provider connection + attributes: + type: object + additionalProperties: + type: array + items: + type: string + description: >- + Key-value attributes for ABAC access control + additionalProperties: false + required: + - provider_id + - api + - provider_type + - config + - status + - created_at + - updated_at + - metadata + title: ProviderConnectionInfo + description: >- + Information about a dynamically managed provider connection. + + This model represents a provider that has been registered at runtime + + via the /providers API, as opposed to static providers configured in run.yaml. + + + Dynamic providers support full lifecycle management including registration, + + configuration updates, health monitoring, and removal. + ProviderConnectionStatus: + type: string + enum: + - pending + - initializing + - connected + - failed + - disconnected + - testing + title: ProviderConnectionStatus + description: Status of a dynamic provider connection. + ProviderHealth: + type: object + properties: + status: + type: string + enum: + - OK + - Error + - Not Implemented + description: >- + Health status (OK, ERROR, NOT_IMPLEMENTED) + message: + type: string + description: Optional error or status message + metrics: + type: object + additionalProperties: + oneOf: + - type: 'null' + - type: boolean + - type: number + - type: string + - type: array + - type: object + description: Provider-specific health metrics + last_checked: + type: string + format: date-time + description: Timestamp of last health check + additionalProperties: false + required: + - status + - metrics + - last_checked + title: ProviderHealth + description: >- + Structured wrapper around provider health status. + + This wraps the existing dict-based HealthResponse for API responses + + while maintaining backward compatibility with existing provider implementations. + RegisterProviderResponse: + type: object + properties: + provider: + $ref: '#/components/schemas/ProviderConnectionInfo' + description: >- + Information about the registered provider + additionalProperties: false + required: + - provider + title: RegisterProviderResponse + description: Response after registering a provider. + UpdateProviderRequest: + type: object + properties: + config: + type: object + additionalProperties: + oneOf: + - type: 'null' + - type: boolean + - type: number + - type: string + - type: array + - type: object + description: >- + New configuration parameters (merged with existing) + attributes: + type: object + additionalProperties: + type: array + items: + type: string + description: New attributes for access control + additionalProperties: false + title: UpdateProviderRequest + UpdateProviderResponse: + type: object + properties: + provider: + $ref: '#/components/schemas/ProviderConnectionInfo' + description: Updated provider information + additionalProperties: false + required: + - provider + title: UpdateProviderResponse + description: Response after updating a provider. Order: type: string enum: @@ -5467,6 +5863,32 @@ 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 2616a9917..0ce5be819 100644 --- a/docs/static/stainless-llama-stack-spec.html +++ b/docs/static/stainless-llama-stack-spec.html @@ -40,6 +40,142 @@ } ], "paths": { + "/v1/admin/providers": { + "post": { + "responses": { + "200": { + "description": "RegisterProviderResponse with the registered provider info.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterProviderResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest400" + }, + "429": { + "$ref": "#/components/responses/TooManyRequests429" + }, + "500": { + "$ref": "#/components/responses/InternalServerError500" + }, + "default": { + "$ref": "#/components/responses/DefaultError" + } + }, + "tags": [ + "Providers" + ], + "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": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterProviderRequest" + } + } + }, + "required": true + }, + "deprecated": false + } + }, + "/v1/admin/providers/{provider_id}": { + "post": { + "responses": { + "200": { + "description": "UpdateProviderResponse with updated provider info", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateProviderResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest400" + }, + "429": { + "$ref": "#/components/responses/TooManyRequests429" + }, + "500": { + "$ref": "#/components/responses/InternalServerError500" + }, + "default": { + "$ref": "#/components/responses/DefaultError" + } + }, + "tags": [ + "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.", + "parameters": [ + { + "name": "provider_id", + "in": "path", + "description": "ID of the provider to update", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateProviderRequest" + } + } + }, + "required": true + }, + "deprecated": false + }, + "delete": { + "responses": { + "200": { + "description": "OK" + }, + "400": { + "$ref": "#/components/responses/BadRequest400" + }, + "429": { + "$ref": "#/components/responses/TooManyRequests429" + }, + "500": { + "$ref": "#/components/responses/InternalServerError500" + }, + "default": { + "$ref": "#/components/responses/DefaultError" + } + }, + "tags": [ + "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.", + "parameters": [ + { + "name": "provider_id", + "in": "path", + "description": "ID of the provider to unregister.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "deprecated": false + } + }, "/v1/chat/completions": { "get": { "responses": { @@ -1680,6 +1816,51 @@ "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.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "deprecated": false + } + }, "/v1/responses": { "get": { "responses": { @@ -5677,6 +5858,351 @@ "title": "Error", "description": "Error response from the API. Roughly follows RFC 7807." }, + "RegisterProviderRequest": { + "type": "object", + "properties": { + "provider_id": { + "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')." + }, + "config": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "array" + }, + { + "type": "object" + } + ] + }, + "description": "Provider configuration (API keys, endpoints, etc.)." + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Optional attributes for ABAC access control." + } + }, + "additionalProperties": false, + "required": [ + "provider_id", + "api", + "provider_type", + "config" + ], + "title": "RegisterProviderRequest" + }, + "ProviderConnectionInfo": { + "type": "object", + "properties": { + "provider_id": { + "type": "string", + "description": "Unique identifier for this provider instance" + }, + "api": { + "type": "string", + "description": "API namespace (e.g., \"inference\", \"vector_io\", \"safety\")" + }, + "provider_type": { + "type": "string", + "description": "Provider type identifier (e.g., \"remote::openai\", \"inline::faiss\")" + }, + "config": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "array" + }, + { + "type": "object" + } + ] + }, + "description": "Provider-specific configuration (API keys, endpoints, etc.)" + }, + "status": { + "$ref": "#/components/schemas/ProviderConnectionStatus", + "description": "Current connection status" + }, + "health": { + "$ref": "#/components/schemas/ProviderHealth", + "description": "Most recent health check result" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when provider was registered" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of last update" + }, + "last_health_check": { + "type": "string", + "format": "date-time", + "description": "Timestamp of last health check" + }, + "error_message": { + "type": "string", + "description": "Error message if status is failed" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "array" + }, + { + "type": "object" + } + ] + }, + "description": "User-defined metadata (deprecated, use attributes)" + }, + "owner": { + "type": "object", + "properties": { + "principal": { + "type": "string" + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "additionalProperties": false, + "required": [ + "principal" + ], + "description": "User who created this provider connection" + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Key-value attributes for ABAC access control" + } + }, + "additionalProperties": false, + "required": [ + "provider_id", + "api", + "provider_type", + "config", + "status", + "created_at", + "updated_at", + "metadata" + ], + "title": "ProviderConnectionInfo", + "description": "Information about a dynamically managed provider connection.\nThis model represents a provider that has been registered at runtime\nvia the /providers API, as opposed to static providers configured in run.yaml.\n\nDynamic providers support full lifecycle management including registration,\nconfiguration updates, health monitoring, and removal." + }, + "ProviderConnectionStatus": { + "type": "string", + "enum": [ + "pending", + "initializing", + "connected", + "failed", + "disconnected", + "testing" + ], + "title": "ProviderConnectionStatus", + "description": "Status of a dynamic provider connection." + }, + "ProviderHealth": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "OK", + "Error", + "Not Implemented" + ], + "description": "Health status (OK, ERROR, NOT_IMPLEMENTED)" + }, + "message": { + "type": "string", + "description": "Optional error or status message" + }, + "metrics": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "array" + }, + { + "type": "object" + } + ] + }, + "description": "Provider-specific health metrics" + }, + "last_checked": { + "type": "string", + "format": "date-time", + "description": "Timestamp of last health check" + } + }, + "additionalProperties": false, + "required": [ + "status", + "metrics", + "last_checked" + ], + "title": "ProviderHealth", + "description": "Structured wrapper around provider health status.\nThis wraps the existing dict-based HealthResponse for API responses\nwhile maintaining backward compatibility with existing provider implementations." + }, + "RegisterProviderResponse": { + "type": "object", + "properties": { + "provider": { + "$ref": "#/components/schemas/ProviderConnectionInfo", + "description": "Information about the registered provider" + } + }, + "additionalProperties": false, + "required": [ + "provider" + ], + "title": "RegisterProviderResponse", + "description": "Response after registering a provider." + }, + "UpdateProviderRequest": { + "type": "object", + "properties": { + "config": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "array" + }, + { + "type": "object" + } + ] + }, + "description": "New configuration parameters (merged with existing)" + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "New attributes for access control" + } + }, + "additionalProperties": false, + "title": "UpdateProviderRequest" + }, + "UpdateProviderResponse": { + "type": "object", + "properties": { + "provider": { + "$ref": "#/components/schemas/ProviderConnectionInfo", + "description": "Updated provider information" + } + }, + "additionalProperties": false, + "required": [ + "provider" + ], + "title": "UpdateProviderResponse", + "description": "Response after updating a provider." + }, "Order": { "type": "string", "enum": [ @@ -8914,6 +9440,51 @@ "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 7b03cd03e..2953055a5 100644 --- a/docs/static/stainless-llama-stack-spec.yaml +++ b/docs/static/stainless-llama-stack-spec.yaml @@ -15,6 +15,120 @@ info: servers: - url: http://any-hosted-llama-stack.com paths: + /v1/admin/providers: + post: + responses: + '200': + description: >- + RegisterProviderResponse with the registered provider info. + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterProviderResponse' + '400': + $ref: '#/components/responses/BadRequest400' + '429': + $ref: >- + #/components/responses/TooManyRequests429 + '500': + $ref: >- + #/components/responses/InternalServerError500 + default: + $ref: '#/components/responses/DefaultError' + tags: + - Providers + summary: Register a new dynamic provider. + description: >- + Register a new dynamic provider. + + Register a new provider instance at runtime. The provider will be validated, + + instantiated, and persisted to the kvstore. Requires appropriate ABAC permissions. + parameters: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterProviderRequest' + required: true + deprecated: false + /v1/admin/providers/{provider_id}: + post: + responses: + '200': + description: >- + UpdateProviderResponse with updated provider info + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateProviderResponse' + '400': + $ref: '#/components/responses/BadRequest400' + '429': + $ref: >- + #/components/responses/TooManyRequests429 + '500': + $ref: >- + #/components/responses/InternalServerError500 + default: + $ref: '#/components/responses/DefaultError' + tags: + - Providers + summary: >- + Update an existing provider's configuration. + description: >- + 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. + parameters: + - name: provider_id + in: path + description: ID of the provider to update + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateProviderRequest' + required: true + deprecated: false + delete: + responses: + '200': + description: OK + '400': + $ref: '#/components/responses/BadRequest400' + '429': + $ref: >- + #/components/responses/TooManyRequests429 + '500': + $ref: >- + #/components/responses/InternalServerError500 + default: + $ref: '#/components/responses/DefaultError' + tags: + - Providers + summary: Unregister a dynamic provider. + description: >- + 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. + parameters: + - name: provider_id + in: path + description: ID of the provider to unregister. + required: true + schema: + type: string + deprecated: false /v1/chat/completions: get: responses: @@ -1289,6 +1403,43 @@ paths: 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. + required: true + schema: + type: string + deprecated: false /v1/responses: get: responses: @@ -4212,6 +4363,251 @@ components: title: Error description: >- Error response from the API. Roughly follows RFC 7807. + RegisterProviderRequest: + type: object + properties: + provider_id: + 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'). + config: + type: object + additionalProperties: + oneOf: + - type: 'null' + - type: boolean + - type: number + - type: string + - type: array + - type: object + description: >- + Provider configuration (API keys, endpoints, etc.). + attributes: + type: object + additionalProperties: + type: array + items: + type: string + description: >- + Optional attributes for ABAC access control. + additionalProperties: false + required: + - provider_id + - api + - provider_type + - config + title: RegisterProviderRequest + ProviderConnectionInfo: + type: object + properties: + provider_id: + type: string + description: >- + Unique identifier for this provider instance + api: + type: string + description: >- + API namespace (e.g., "inference", "vector_io", "safety") + provider_type: + type: string + description: >- + Provider type identifier (e.g., "remote::openai", "inline::faiss") + config: + type: object + additionalProperties: + oneOf: + - type: 'null' + - type: boolean + - type: number + - type: string + - type: array + - type: object + description: >- + Provider-specific configuration (API keys, endpoints, etc.) + status: + $ref: '#/components/schemas/ProviderConnectionStatus' + description: Current connection status + health: + $ref: '#/components/schemas/ProviderHealth' + description: Most recent health check result + created_at: + type: string + format: date-time + description: Timestamp when provider was registered + updated_at: + type: string + format: date-time + description: Timestamp of last update + last_health_check: + type: string + format: date-time + description: Timestamp of last health check + error_message: + type: string + description: Error message if status is failed + metadata: + type: object + additionalProperties: + oneOf: + - type: 'null' + - type: boolean + - type: number + - type: string + - type: array + - type: object + description: >- + User-defined metadata (deprecated, use attributes) + owner: + type: object + properties: + principal: + type: string + attributes: + type: object + additionalProperties: + type: array + items: + type: string + additionalProperties: false + required: + - principal + description: >- + User who created this provider connection + attributes: + type: object + additionalProperties: + type: array + items: + type: string + description: >- + Key-value attributes for ABAC access control + additionalProperties: false + required: + - provider_id + - api + - provider_type + - config + - status + - created_at + - updated_at + - metadata + title: ProviderConnectionInfo + description: >- + Information about a dynamically managed provider connection. + + This model represents a provider that has been registered at runtime + + via the /providers API, as opposed to static providers configured in run.yaml. + + + Dynamic providers support full lifecycle management including registration, + + configuration updates, health monitoring, and removal. + ProviderConnectionStatus: + type: string + enum: + - pending + - initializing + - connected + - failed + - disconnected + - testing + title: ProviderConnectionStatus + description: Status of a dynamic provider connection. + ProviderHealth: + type: object + properties: + status: + type: string + enum: + - OK + - Error + - Not Implemented + description: >- + Health status (OK, ERROR, NOT_IMPLEMENTED) + message: + type: string + description: Optional error or status message + metrics: + type: object + additionalProperties: + oneOf: + - type: 'null' + - type: boolean + - type: number + - type: string + - type: array + - type: object + description: Provider-specific health metrics + last_checked: + type: string + format: date-time + description: Timestamp of last health check + additionalProperties: false + required: + - status + - metrics + - last_checked + title: ProviderHealth + description: >- + Structured wrapper around provider health status. + + This wraps the existing dict-based HealthResponse for API responses + + while maintaining backward compatibility with existing provider implementations. + RegisterProviderResponse: + type: object + properties: + provider: + $ref: '#/components/schemas/ProviderConnectionInfo' + description: >- + Information about the registered provider + additionalProperties: false + required: + - provider + title: RegisterProviderResponse + description: Response after registering a provider. + UpdateProviderRequest: + type: object + properties: + config: + type: object + additionalProperties: + oneOf: + - type: 'null' + - type: boolean + - type: number + - type: string + - type: array + - type: object + description: >- + New configuration parameters (merged with existing) + attributes: + type: object + additionalProperties: + type: array + items: + type: string + description: New attributes for access control + additionalProperties: false + title: UpdateProviderRequest + UpdateProviderResponse: + type: object + properties: + provider: + $ref: '#/components/schemas/ProviderConnectionInfo' + description: Updated provider information + additionalProperties: false + required: + - provider + title: UpdateProviderResponse + description: Response after updating a provider. Order: type: string enum: @@ -6680,6 +7076,32 @@ 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/connection.py b/llama_stack/apis/providers/connection.py new file mode 100644 index 000000000..791c46e74 --- /dev/null +++ b/llama_stack/apis/providers/connection.py @@ -0,0 +1,117 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the terms described in the LICENSE file in +# the root directory of this source tree. + +from datetime import UTC, datetime +from enum import StrEnum +from typing import Any + +from pydantic import BaseModel, Field + +from llama_stack.core.datatypes import User +from llama_stack.providers.datatypes import HealthStatus +from llama_stack.schema_utils import json_schema_type + + +@json_schema_type +class ProviderConnectionStatus(StrEnum): + """Status of a dynamic provider connection. + + :cvar pending: Configuration stored, not yet initialized + :cvar initializing: In the process of connecting + :cvar connected: Successfully connected and healthy + :cvar failed: Connection attempt failed + :cvar disconnected: Previously connected, now disconnected + :cvar testing: Health check in progress + """ + + pending = "pending" + initializing = "initializing" + connected = "connected" + failed = "failed" + disconnected = "disconnected" + testing = "testing" + + +@json_schema_type +class ProviderHealth(BaseModel): + """Structured wrapper around provider health status. + + This wraps the existing dict-based HealthResponse for API responses + while maintaining backward compatibility with existing provider implementations. + + :param status: Health status (OK, ERROR, NOT_IMPLEMENTED) + :param message: Optional error or status message + :param metrics: Provider-specific health metrics + :param last_checked: Timestamp of last health check + """ + + status: HealthStatus + message: str | None = None + metrics: dict[str, Any] = Field(default_factory=dict) + last_checked: datetime + + @classmethod + def from_health_response(cls, response: dict[str, Any]) -> "ProviderHealth": + """Convert dict-based HealthResponse to ProviderHealth. + + This allows us to maintain the existing dict[str, Any] return type + for provider.health() methods while providing a structured model + for API responses. + + :param response: Dict with 'status' and optional 'message', 'metrics' + :returns: ProviderHealth instance + """ + return cls( + status=HealthStatus(response.get("status", HealthStatus.NOT_IMPLEMENTED)), + message=response.get("message"), + metrics=response.get("metrics", {}), + last_checked=datetime.now(UTC), + ) + + +@json_schema_type +class ProviderConnectionInfo(BaseModel): + """Information about a dynamically managed provider connection. + + This model represents a provider that has been registered at runtime + via the /providers API, as opposed to static providers configured in run.yaml. + + Dynamic providers support full lifecycle management including registration, + configuration updates, health monitoring, and removal. + + :param provider_id: Unique identifier for this provider instance + :param api: API namespace (e.g., "inference", "vector_io", "safety") + :param provider_type: Provider type identifier (e.g., "remote::openai", "inline::faiss") + :param config: Provider-specific configuration (API keys, endpoints, etc.) + :param status: Current connection status + :param health: Most recent health check result + :param created_at: Timestamp when provider was registered + :param updated_at: Timestamp of last update + :param last_health_check: Timestamp of last health check + :param error_message: Error message if status is failed + :param metadata: User-defined metadata (deprecated, use attributes) + :param owner: User who created this provider connection + :param attributes: Key-value attributes for ABAC access control + """ + + provider_id: str + api: str + provider_type: str + config: dict[str, Any] + status: ProviderConnectionStatus + health: ProviderHealth | None = None + created_at: datetime + updated_at: datetime + last_health_check: datetime | None = None + error_message: str | None = None + metadata: dict[str, Any] = Field( + default_factory=dict, + description="Deprecated: use attributes for access control", + ) + + # ABAC fields (same as ResourceWithOwner) + owner: User | None = None + attributes: dict[str, list[str]] | None = None diff --git a/llama_stack/apis/providers/providers.py b/llama_stack/apis/providers/providers.py index e1872571d..c52a15e0c 100644 --- a/llama_stack/apis/providers/providers.py +++ b/llama_stack/apis/providers/providers.py @@ -8,6 +8,7 @@ from typing import Any, Protocol, runtime_checkable from pydantic import BaseModel +from llama_stack.apis.providers.connection import ProviderConnectionInfo from llama_stack.apis.version import LLAMA_STACK_API_V1 from llama_stack.providers.datatypes import HealthResponse from llama_stack.schema_utils import json_schema_type, webmethod @@ -40,6 +41,85 @@ class ListProvidersResponse(BaseModel): data: list[ProviderInfo] +# ===== Dynamic Provider Management API Models ===== + + +@json_schema_type +class RegisterProviderRequest(BaseModel): + """Request to register a new dynamic provider. + + :param provider_id: Unique identifier for the provider instance + :param api: API namespace (e.g., 'inference', 'vector_io', 'safety') + :param provider_type: Provider type identifier (e.g., 'remote::openai', 'inline::faiss') + :param config: Provider-specific configuration (API keys, endpoints, etc.) + :param attributes: Optional key-value attributes for ABAC access control + """ + + provider_id: str + api: str + provider_type: str + config: dict[str, Any] + attributes: dict[str, list[str]] | None = None + + +@json_schema_type +class RegisterProviderResponse(BaseModel): + """Response after registering a provider. + + :param provider: Information about the registered provider + """ + + provider: ProviderConnectionInfo + + +@json_schema_type +class UpdateProviderRequest(BaseModel): + """Request to update an existing provider's configuration. + + :param config: New configuration parameters (will be merged with existing) + :param attributes: Optional updated attributes for access control + """ + + config: dict[str, Any] | None = None + attributes: dict[str, list[str]] | None = None + + +@json_schema_type +class UpdateProviderResponse(BaseModel): + """Response after updating a provider. + + :param provider: Updated provider information + """ + + provider: ProviderConnectionInfo + + +@json_schema_type +class UnregisterProviderResponse(BaseModel): + """Response after unregistering a provider. + + :param success: Whether the operation succeeded + :param message: Optional status message + """ + + success: bool + message: str | None = None + + +@json_schema_type +class TestProviderConnectionResponse(BaseModel): + """Response from testing a provider connection. + + :param success: Whether the connection test succeeded + :param health: Health status from the provider + :param error_message: Error message if test failed + """ + + success: bool + health: HealthResponse | None = None + error_message: str | None = None + + @runtime_checkable class Providers(Protocol): """Providers @@ -67,3 +147,71 @@ class Providers(Protocol): :returns: A ProviderInfo object containing the provider's details. """ ... + + # ===== Dynamic Provider Management Methods ===== + + @webmethod(route="/admin/providers", method="POST", level=LLAMA_STACK_API_V1) + async def register_provider( + self, + provider_id: str, + api: str, + provider_type: str, + config: dict[str, Any], + attributes: dict[str, list[str]] | None = None, + ) -> RegisterProviderResponse: + """Register a new dynamic provider. + + Register a new provider instance at runtime. The provider will be validated, + instantiated, and persisted to the kvstore. Requires appropriate ABAC permissions. + + :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. + :returns: RegisterProviderResponse with the registered provider info. + """ + ... + + @webmethod(route="/admin/providers/{provider_id}", method="PUT", level=LLAMA_STACK_API_V1) + async def update_provider( + self, + provider_id: str, + config: dict[str, Any] | None = None, + attributes: dict[str, list[str]] | None = None, + ) -> UpdateProviderResponse: + """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. + + :param provider_id: ID of the provider to update + :param config: New configuration parameters (merged with existing) + :param attributes: New attributes for access control + :returns: UpdateProviderResponse with updated provider info + """ + ... + + @webmethod(route="/admin/providers/{provider_id}", method="DELETE", level=LLAMA_STACK_API_V1) + async def unregister_provider(self, 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. + + :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: + """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 provider_id: ID of the provider to test. + :returns: TestProviderConnectionResponse with health status. + """ + ... diff --git a/llama_stack/core/providers.py b/llama_stack/core/providers.py index 7095ffd18..ee432e793 100644 --- a/llama_stack/core/providers.py +++ b/llama_stack/core/providers.py @@ -5,22 +5,43 @@ # the root directory of this source tree. import asyncio +from datetime import UTC, datetime from typing import Any from pydantic import BaseModel -from llama_stack.apis.providers import ListProvidersResponse, ProviderInfo, Providers +from llama_stack.apis.providers import ( + ListProvidersResponse, + ProviderInfo, + Providers, + RegisterProviderResponse, + TestProviderConnectionResponse, + UpdateProviderResponse, +) +from llama_stack.apis.providers.connection import ( + ProviderConnectionInfo, + ProviderConnectionStatus, + ProviderHealth, +) +from llama_stack.core.request_headers import get_authenticated_user +from llama_stack.core.resolver import ProviderWithSpec, instantiate_provider from llama_stack.log import get_logger -from llama_stack.providers.datatypes import HealthResponse, HealthStatus +from llama_stack.providers.datatypes import Api, HealthResponse, HealthStatus from .datatypes import StackRunConfig from .utils.config import redact_sensitive_fields logger = get_logger(name=__name__, category="core") +# Storage constants for dynamic provider connections +PROVIDER_CONNECTIONS_PREFIX = "provider_connections:v1::" + class ProviderImplConfig(BaseModel): run_config: StackRunConfig + provider_registry: Any | None = None # ProviderRegistry from resolver + dist_registry: Any | None = None # DistributionRegistry + policy: list[Any] | None = None # list[AccessRule] async def get_provider_impl(config, deps): @@ -33,19 +54,71 @@ class ProviderImpl(Providers): def __init__(self, config, deps): self.config = config self.deps = deps + self.kvstore = None # KVStore for dynamic provider persistence + self.dynamic_providers: dict[str, ProviderConnectionInfo] = {} # Runtime cache + self.dynamic_provider_impls: dict[str, Any] = {} # Initialized provider instances + + # Store registry references for provider instantiation + self.provider_registry = config.provider_registry + self.dist_registry = config.dist_registry + self.policy = config.policy or [] async def initialize(self) -> None: - pass + # Initialize kvstore for dynamic providers + # Reuse the same kvstore as the distribution registry if available + if hasattr(self.config.run_config, "metadata_store") and self.config.run_config.metadata_store: + from llama_stack.providers.utils.kvstore import kvstore_impl + + self.kvstore = await kvstore_impl(self.config.run_config.metadata_store) + 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)} 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") + else: + logger.warning("No metadata_store configured, dynamic provider management disabled") async def shutdown(self) -> None: logger.debug("ProviderImpl.shutdown") - pass + + # Shutdown all dynamic provider instances + for provider_id, impl in self.dynamic_provider_impls.items(): + try: + if hasattr(impl, "shutdown"): + await impl.shutdown() + logger.debug(f"Shutdown dynamic provider {provider_id}") + except Exception as e: + logger.warning(f"Error shutting down dynamic provider {provider_id}: {e}") + + # Shutdown kvstore + if self.kvstore and hasattr(self.kvstore, "shutdown"): + await self.kvstore.shutdown() async def list_providers(self) -> ListProvidersResponse: run_config = self.config.run_config safe_config = StackRunConfig(**redact_sensitive_fields(run_config.model_dump())) providers_health = await self.get_providers_health() ret = [] + + # Add static providers (from run.yaml) for api, providers in safe_config.providers.items(): for p in providers: # Skip providers that are not enabled @@ -66,6 +139,32 @@ class ProviderImpl(Providers): ) ) + # Add dynamic providers (from kvstore) + for _provider_id, conn_info in self.dynamic_providers.items(): + # Redact sensitive config for API response + redacted_config = self._redact_sensitive_config(conn_info.config) + + # Convert ProviderHealth to HealthResponse dict for API compatibility + health_dict: HealthResponse | None = None + if conn_info.health: + health_dict = HealthResponse( + status=conn_info.health.status, + message=conn_info.health.message, + ) + if conn_info.health.metrics: + health_dict["metrics"] = conn_info.health.metrics + + ret.append( + ProviderInfo( + api=conn_info.api, + provider_id=conn_info.provider_id, + provider_type=conn_info.provider_type, + config=redacted_config, + health=health_dict + or HealthResponse(status=HealthStatus.NOT_IMPLEMENTED, message="No health check available"), + ) + ) + return ListProvidersResponse(data=ret) async def inspect_provider(self, provider_id: str) -> ProviderInfo: @@ -135,3 +234,378 @@ class ProviderImpl(Providers): providers_health[api_name] = health_response return providers_health + + # Storage helper methods for dynamic providers + + async def _store_connection(self, info: ProviderConnectionInfo) -> None: + """Store provider connection info in kvstore. + + :param info: ProviderConnectionInfo to store + """ + if not self.kvstore: + raise RuntimeError("KVStore not initialized") + + key = f"{PROVIDER_CONNECTIONS_PREFIX}{info.provider_id}" + await self.kvstore.set(key, info.model_dump_json()) + logger.debug(f"Stored provider connection: {info.provider_id}") + + async def _load_connection(self, provider_id: str) -> ProviderConnectionInfo | None: + """Load provider connection info from kvstore. + + :param provider_id: Provider ID to load + :returns: ProviderConnectionInfo if found, None otherwise + """ + if not self.kvstore: + return None + + key = f"{PROVIDER_CONNECTIONS_PREFIX}{provider_id}" + value = await self.kvstore.get(key) + if value: + return ProviderConnectionInfo.model_validate_json(value) + return None + + async def _delete_connection(self, provider_id: str) -> None: + """Delete provider connection from kvstore. + + :param provider_id: Provider ID to delete + """ + if not self.kvstore: + raise RuntimeError("KVStore not initialized") + + key = f"{PROVIDER_CONNECTIONS_PREFIX}{provider_id}" + await self.kvstore.delete(key) + logger.debug(f"Deleted provider connection: {provider_id}") + + async def _list_connections(self) -> list[ProviderConnectionInfo]: + """List all dynamic provider connections from kvstore. + + :returns: List of ProviderConnectionInfo + """ + if not self.kvstore: + return [] + + start_key = PROVIDER_CONNECTIONS_PREFIX + end_key = f"{PROVIDER_CONNECTIONS_PREFIX}\xff" + values = await self.kvstore.values_in_range(start_key, end_key) + return [ProviderConnectionInfo.model_validate_json(v) for v in values] + + async def _load_dynamic_providers(self) -> None: + """Load dynamic providers from kvstore into runtime cache.""" + connections = await self._list_connections() + for conn in connections: + self.dynamic_providers[conn.provider_id] = conn + logger.debug(f"Loaded dynamic provider: {conn.provider_id} (status: {conn.status})") + + # Helper methods for dynamic provider management + + def _redact_sensitive_config(self, config: dict[str, Any]) -> dict[str, Any]: + """Redact sensitive fields in provider config for API responses. + + :param config: Provider configuration dict + :returns: Config with sensitive fields redacted + """ + return redact_sensitive_fields(config) + + async def _instantiate_provider(self, conn_info: ProviderConnectionInfo) -> Any: + """Instantiate a provider from connection info. + + Uses the resolver's instantiate_provider() to create a provider instance + with all necessary dependencies. + + :param conn_info: Provider connection information + :returns: Instantiated provider implementation + :raises RuntimeError: If provider cannot be instantiated + """ + if not self.provider_registry: + raise RuntimeError("Provider registry not available for provider instantiation") + if not self.dist_registry: + raise RuntimeError("Distribution registry not available for provider instantiation") + + # Get provider spec from registry + api = Api(conn_info.api) + if api not in self.provider_registry: + raise ValueError(f"API {conn_info.api} not found in provider registry") + + if conn_info.provider_type not in self.provider_registry[api]: + raise ValueError(f"Provider type {conn_info.provider_type} not found for API {conn_info.api}") + + provider_spec = self.provider_registry[api][conn_info.provider_type] + + # Create ProviderWithSpec for instantiation + provider_with_spec = ProviderWithSpec( + provider_id=conn_info.provider_id, + provider_type=conn_info.provider_type, + config=conn_info.config, + spec=provider_spec, + ) + + # Resolve dependencies + deps = {} + for dep_api in provider_spec.api_dependencies: + if dep_api not in self.deps: + raise RuntimeError( + f"Required dependency {dep_api.value} not available for provider {conn_info.provider_id}" + ) + deps[dep_api] = self.deps[dep_api] + + # Add optional dependencies if available + for dep_api in provider_spec.optional_api_dependencies: + if dep_api in self.deps: + deps[dep_api] = self.deps[dep_api] + + # Instantiate provider using resolver + impl = await instantiate_provider( + provider_with_spec, + deps, + {}, # inner_impls (empty for dynamic providers) + self.dist_registry, + self.config.run_config, + self.policy, + ) + + logger.debug(f"Instantiated provider {conn_info.provider_id} (type={conn_info.provider_type})") + return impl + + # Dynamic Provider Management Methods + + async def register_provider( + self, + provider_id: str, + api: str, + provider_type: str, + config: dict[str, Any], + attributes: dict[str, list[str]] | None = None, + ) -> RegisterProviderResponse: + """Register a new provider. + + This is used both for: + - Providers from run.yaml (registered at startup) + - Providers registered via API (registered at runtime) + + All providers are stored in kvstore and treated equally. + """ + if not self.kvstore: + raise RuntimeError("Dynamic provider management is not enabled (no kvstore configured)") + + # Check if provider_id already exists + if provider_id in self.dynamic_providers: + raise ValueError(f"Provider {provider_id} already exists") + + # Get authenticated user as owner + user = get_authenticated_user() + + # Create ProviderConnectionInfo + now = datetime.now(UTC) + conn_info = ProviderConnectionInfo( + provider_id=provider_id, + api=api, + provider_type=provider_type, + config=config, + status=ProviderConnectionStatus.initializing, + created_at=now, + updated_at=now, + owner=user, + attributes=attributes, + ) + + try: + # 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) + self.dynamic_provider_impls[provider_id] = impl + + # 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)") + + # Store updated status + await self._store_connection(conn_info) + + # Add to runtime cache + self.dynamic_providers[provider_id] = conn_info + + return RegisterProviderResponse(provider=conn_info) + + except Exception as e: + # Mark as failed and store + conn_info.status = ProviderConnectionStatus.failed + conn_info.error_message = str(e) + conn_info.updated_at = datetime.now(UTC) + await self._store_connection(conn_info) + self.dynamic_providers[provider_id] = conn_info + + logger.error(f"Failed to register provider {provider_id}: {e}") + raise RuntimeError(f"Failed to register provider: {e}") from e + + async def update_provider( + self, + provider_id: str, + config: dict[str, Any] | None = None, + attributes: dict[str, list[str]] | None = None, + ) -> UpdateProviderResponse: + """Update an existing provider's configuration. + + Updates persist to kvstore and survive server restarts. + This works for all providers (whether originally from run.yaml or API). + """ + 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") + + conn_info = self.dynamic_providers[provider_id] + + # Update config if provided + if config is not None: + conn_info.config.update(config) + + # Update attributes if provided + if attributes is not None: + conn_info.attributes = attributes + + conn_info.updated_at = datetime.now(UTC) + conn_info.status = ProviderConnectionStatus.initializing + + try: + # Store updated config + 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}") + + # Reinstantiate with new config + impl = await self._instantiate_provider(conn_info) + self.dynamic_provider_impls[provider_id] = 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) + + 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)") + + return UpdateProviderResponse(provider=conn_info) + + except Exception as e: + conn_info.status = ProviderConnectionStatus.failed + conn_info.error_message = str(e) + conn_info.updated_at = datetime.now(UTC) + await self._store_connection(conn_info) + + 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: + """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). + """ + 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") + + try: + # Shutdown provider instance if it exists + if provider_id in self.dynamic_provider_impls: + impl = self.dynamic_provider_impls[provider_id] + if hasattr(impl, "shutdown"): + await impl.shutdown() + del self.dynamic_provider_impls[provider_id] + + # Remove from kvstore + await self._delete_connection(provider_id) + + # Remove from runtime cache + del self.dynamic_providers[provider_id] + + logger.info(f"Unregistered dynamic provider {provider_id}") + + except Exception as e: + 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: + """Test a provider connection.""" + # Check if provider exists (static or dynamic) + provider_impl = None + + # 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: + 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" + ) + + # Check if provider has health method + if not hasattr(provider_impl, "health"): + return TestProviderConnectionResponse( + success=False, + health=HealthResponse( + status=HealthStatus.NOT_IMPLEMENTED, message="Provider does not implement health check" + ), + ) + + # Call health check + try: + 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] + conn_info.health = ProviderHealth.from_health_response(health_result) + conn_info.last_health_check = datetime.now(UTC) + await self._store_connection(conn_info) + + logger.debug(f"Tested provider connection {provider_id}: status={health_result.get('status', 'UNKNOWN')}") + + return TestProviderConnectionResponse( + success=health_result.get("status") == HealthStatus.OK, + health=health_result, + ) + + except TimeoutError: + health = HealthResponse(status=HealthStatus.ERROR, message="Health check timed out after 5 seconds") + return TestProviderConnectionResponse(success=False, health=health) + + except Exception as e: + health = HealthResponse(status=HealthStatus.ERROR, message=f"Health check failed: {str(e)}") + return TestProviderConnectionResponse(success=False, health=health, error_message=str(e)) diff --git a/llama_stack/core/stack.py b/llama_stack/core/stack.py index ebfd59a05..fb0089432 100644 --- a/llama_stack/core/stack.py +++ b/llama_stack/core/stack.py @@ -341,12 +341,21 @@ def cast_image_name_to_string(config_dict: dict[str, Any]) -> dict[str, Any]: return config_dict -def add_internal_implementations(impls: dict[Api, Any], run_config: StackRunConfig) -> None: +def add_internal_implementations( + impls: dict[Api, Any], + run_config: StackRunConfig, + provider_registry=None, + dist_registry=None, + policy=None, +) -> None: """Add internal implementations (inspect and providers) to the implementations dictionary. Args: impls: Dictionary of API implementations run_config: Stack run configuration + provider_registry: Provider registry for dynamic provider instantiation + dist_registry: Distribution registry + policy: Access control policy """ inspect_impl = DistributionInspectImpl( DistributionInspectConfig(run_config=run_config), @@ -355,7 +364,12 @@ def add_internal_implementations(impls: dict[Api, Any], run_config: StackRunConf impls[Api.inspect] = inspect_impl providers_impl = ProviderImpl( - ProviderImplConfig(run_config=run_config), + ProviderImplConfig( + run_config=run_config, + provider_registry=provider_registry, + dist_registry=dist_registry, + policy=policy, + ), deps=impls, ) impls[Api.providers] = providers_impl @@ -416,13 +430,20 @@ class Stack: raise ValueError("storage.stores.metadata must be configured with a kv_* backend") dist_registry, _ = await create_dist_registry(stores.metadata, self.run_config.image_name) policy = self.run_config.server.auth.access_policy if self.run_config.server.auth else [] + provider_registry = self.provider_registry or get_provider_registry(self.run_config) internal_impls = {} - add_internal_implementations(internal_impls, self.run_config) + add_internal_implementations( + internal_impls, + self.run_config, + provider_registry=provider_registry, + dist_registry=dist_registry, + policy=policy, + ) impls = await resolve_impls( self.run_config, - self.provider_registry or get_provider_registry(self.run_config), + provider_registry, dist_registry, policy, internal_impls,