From 4df953fc86b710536220b71246515d6999e5e4f3 Mon Sep 17 00:00:00 2001 From: JR Boos Date: Mon, 31 Aug 2026 14:00:51 -0400 Subject: [PATCH 001/120] feat(config): added granite guardian configuration --- src/models/config.py | 140 ++++++++++++++++++++++++++++++- src/utils/pydantic_ai_helpers.py | 3 + src/utils/shields.py | 14 +++- 3 files changed, 155 insertions(+), 2 deletions(-) diff --git a/src/models/config.py b/src/models/config.py index f69b6f0d3..f9e977c80 100644 --- a/src/models/config.py +++ b/src/models/config.py @@ -3142,8 +3142,146 @@ class RedactionShieldConfiguration(ConfigurationBase): ) +class RiskDefinition(ConfigurationBase): + """ + Definition for a custom risk category. + + Custom risks allow applications to add use-case-specific safety checks + beyond the standard harm, jailbreak, leetspeak, amnesia, and + history_politics checks. + Example: + liability_risk = RiskDefinition( + name="liability", + description="Content requesting legal, medical, or financial advice", + threshold=0.55, + points=["input"], + ) + pii_risk = RiskDefinition( + name="pii_request", + description="User is asking the AI to reveal personal information", + threshold=0.50, + points=["input", "tool"], + ) + Note: + To enable think mode (detailed reasoning) for a risk, add the risk name + to the `thinking_enabled` list in `ModerationConfig`. Do not set + `enable_thinking` directly - it is managed internally. + """ + + name: str = Field( + ..., + title="Risk name", + description="Unique identifier for this risk (e.g., 'liability', 'competitor_mention')", + ) + description: str = Field( + ..., + title="Rist description", + description="Risk definition text passed to Granite Guardian as custom_criteria", + ) + threshold: float = Field( + default=0.65, + ge=0.0, + le=1.0, + title="Risk threshold", + description="Score threshold for flagging (lower = more sensitive)", + ) + enabled: bool = Field( + default=True, title="Risk enabled", description="Whether to run this check" + ) + enable_thinking: bool = Field( + default=False, + title="Risk enable thinking", + description=( + "Internal field - set via ModerationConfig.thinking_enabled list, " + "not directly. When True, Granite Guardian provides detailed " + "reasoning before scoring." + ), + ) + points: list[Literal["input", "output", "tool"]] = Field( + ..., + min_length=1, + title="Guardrail points", + description=( + "Where this risk is evaluated: `input` (user message), " + "`output` (model response), or `tool` (tool/MCP content)." + ), + ) + violation_message: str = Field( + ..., + title="Violation message", + description="Message to be displayed when this risk is violated", + ) + + +class GraniteGuardianConfig(ConfigurationBase): + """Configuration for the Granite Guardian moderation guardrail.""" + + url: str = Field( + ..., title="Base URL", description="The model_id to use for the guard" + ) + + api_key: Optional[SecretStr] = Field( + None, title="Granite Guardian API key", description="API key for the inference" + ) + + max_retries: PositiveInt = Field( + 2, ge=0, le=5, title="Max retries", description="Maximun number of retires" + ) + + timeout: PositiveInt = Field( + 30, ge=5, le=300, title="Timeout", description="Request timeout in seconds" + ) + + verify_ssl: bool | str = Field( + True, + title="Verify SSL", + description=( + "SSL certificate verification. Can be:\n" + " - True: Verify using system CA bundle (default, recommended)\n" + " - False: Disable verification (insecure, for dev only)\n" + " - str: Path to custom CA bundle file (for internal PKI)" + ), + ) + + risks: list[RiskDefinition] = Field( + ..., + title="Defined risks", + description="Risks to be considered while applying this guradrail", + ) + + +class GraniteGuardianShieldConfiguration(ConfigurationBase): + """Configuration for a named Granite Guardian guardrail shield. + + Attributes: + name: Unique, user-facing name identifying this shield instance. + provider_id: Discriminator identifying this as a granite-guardian shield. + config: Granite-guardian-specific configuration. + """ + + name: str = Field( + ..., + title="Shield name", + description="Unique, user-facing name identifying this shield instance.", + ) + + provider_id: Literal["granite_guardian"] = Field( + ..., + title="Shield provider id", + description="Discriminator identifying this as a granite-guardian shield.", + ) + + config: GraniteGuardianConfig = Field( + ..., + title="Shield configuration", + description="Granite-guardian-specific configuration for this shield", + ) + + ShieldConfiguration = Annotated[ - QuestionValidityShieldConfiguration | RedactionShieldConfiguration, + QuestionValidityShieldConfiguration + | RedactionShieldConfiguration + | GraniteGuardianShieldConfiguration, Field(discriminator="provider_id"), ] """Configuration for a single named guardrail shield (question validity or redaction). diff --git a/src/utils/pydantic_ai_helpers.py b/src/utils/pydantic_ai_helpers.py index 2c2940737..629334d98 100644 --- a/src/utils/pydantic_ai_helpers.py +++ b/src/utils/pydantic_ai_helpers.py @@ -16,6 +16,7 @@ from models.common.skills import SkillMetadata from models.common.tools import CatalogTool, CatalogToolParameter from models.config import ( + GraniteGuardianConfig, QuestionValidityConfig, RedactionConfig, ShieldConfiguration, @@ -174,6 +175,8 @@ def _shield_capability(shield: ShieldConfiguration) -> AgentCapability[object]: return QuestionValidity(config=shield.config) case RedactionConfig(): return PiiRedactionCapability(config=shield.config) + case GraniteGuardianConfig(): + raise NotImplementedError("Granite Guardian capability not implemented") case _: raise ValueError( f"Unsupported shield config type for shield '{shield.name}': " diff --git a/src/utils/shields.py b/src/utils/shields.py index d45cbcb23..d805fb481 100644 --- a/src/utils/shields.py +++ b/src/utils/shields.py @@ -21,7 +21,12 @@ ShieldModerationPassed, ShieldModerationResult, ) -from models.config import QuestionValidityConfig, RedactionConfig, ShieldConfiguration +from models.config import ( + GraniteGuardianConfig, + QuestionValidityConfig, + RedactionConfig, + ShieldConfiguration, +) from pydantic_ai_lightspeed.capabilities.base import AbstractSafetyCapability from pydantic_ai_lightspeed.capabilities.question_validity._capability import ( QuestionValidity, @@ -158,6 +163,13 @@ def build_shield(shield_config: ShieldConfiguration) -> AbstractSafetyCapability return QuestionValidity(shield_config.config) case RedactionConfig(): return PiiRedactionCapability(shield_config.config) + case GraniteGuardianConfig(): + raise NotImplementedError("Granite Guardian capability not implemented") + case _: + raise ValueError( + f"Unsupported shield config type for shield '{shield_config.name}': " + f"{type(shield_config.config).__name__}" + ) async def run_shield_moderation( From 6ecd3e47c3810873c89064bf34323fc87cb193f7 Mon Sep 17 00:00:00 2001 From: JR Boos Date: Mon, 31 Aug 2026 14:07:00 -0400 Subject: [PATCH 002/120] docs(config): update openapi.json --- docs/devel_doc/openapi.json | 166 ++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) diff --git a/docs/devel_doc/openapi.json b/docs/devel_doc/openapi.json index e903f7e22..680a3bf70 100644 --- a/docs/devel_doc/openapi.json +++ b/docs/devel_doc/openapi.json @@ -13295,11 +13295,15 @@ }, { "$ref": "#/components/schemas/RedactionShieldConfiguration" + }, + { + "$ref": "#/components/schemas/GraniteGuardianShieldConfiguration" } ], "discriminator": { "propertyName": "provider_id", "mapping": { + "granite_guardian": "#/components/schemas/GraniteGuardianShieldConfiguration", "question_validity": "#/components/schemas/QuestionValidityShieldConfiguration", "redaction": "#/components/schemas/RedactionShieldConfiguration" } @@ -14630,6 +14634,105 @@ } ] }, + "GraniteGuardianConfig": { + "properties": { + "url": { + "type": "string", + "title": "Base URL", + "description": "The model_id to use for the guard" + }, + "api_key": { + "anyOf": [ + { + "type": "string", + "format": "password", + "writeOnly": true + }, + { + "type": "null" + } + ], + "title": "Granite Guardian API key", + "description": "API key for the inference" + }, + "max_retries": { + "type": "integer", + "maximum": 5.0, + "minimum": 0.0, + "exclusiveMinimum": 0.0, + "title": "Max retries", + "description": "Maximun number of retires", + "default": 2 + }, + "timeout": { + "type": "integer", + "maximum": 300.0, + "minimum": 5.0, + "exclusiveMinimum": 0.0, + "title": "Timeout", + "description": "Request timeout in seconds", + "default": 30 + }, + "verify_ssl": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ], + "title": "Verify SSL", + "description": "SSL certificate verification. Can be:\n - True: Verify using system CA bundle (default, recommended)\n - False: Disable verification (insecure, for dev only)\n - str: Path to custom CA bundle file (for internal PKI)", + "default": true + }, + "risks": { + "items": { + "$ref": "#/components/schemas/RiskDefinition" + }, + "type": "array", + "title": "Defined risks", + "description": "Risks to be considered while applying this guradrail" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "url", + "risks" + ], + "title": "GraniteGuardianConfig", + "description": "Configuration for the Granite Guardian moderation guardrail." + }, + "GraniteGuardianShieldConfiguration": { + "properties": { + "name": { + "type": "string", + "title": "Shield name", + "description": "Unique, user-facing name identifying this shield instance." + }, + "provider_id": { + "type": "string", + "const": "granite_guardian", + "title": "Shield provider id", + "description": "Discriminator identifying this as a granite-guardian shield." + }, + "config": { + "$ref": "#/components/schemas/GraniteGuardianConfig", + "title": "Shield configuration", + "description": "Granite-guardian-specific configuration for this shield" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "provider_id", + "config" + ], + "title": "GraniteGuardianShieldConfiguration", + "description": "Configuration for a named Granite Guardian guardrail shield.\n\nAttributes:\n name: Unique, user-facing name identifying this shield instance.\n provider_id: Discriminator identifying this as a granite-guardian shield.\n config: Granite-guardian-specific configuration." + }, "HTTPAuthSecurityScheme": { "properties": { "bearerFormat": { @@ -20718,6 +20821,69 @@ "title": "RetrievalStrategyConfiguration", "description": "Configuration for a single retrieval strategy (inline or tool)." }, + "RiskDefinition": { + "properties": { + "name": { + "type": "string", + "title": "Risk name", + "description": "Unique identifier for this risk (e.g., 'liability', 'competitor_mention')" + }, + "description": { + "type": "string", + "title": "Rist description", + "description": "Risk definition text passed to Granite Guardian as custom_criteria" + }, + "threshold": { + "type": "number", + "maximum": 1.0, + "minimum": 0.0, + "title": "Risk threshold", + "description": "Score threshold for flagging (lower = more sensitive)", + "default": 0.65 + }, + "enabled": { + "type": "boolean", + "title": "Risk enabled", + "description": "Whether to run this check", + "default": true + }, + "enable_thinking": { + "type": "boolean", + "title": "Risk enable thinking", + "description": "Internal field - set via ModerationConfig.thinking_enabled list, not directly. When True, Granite Guardian provides detailed reasoning before scoring.", + "default": false + }, + "points": { + "items": { + "type": "string", + "enum": [ + "input", + "output", + "tool" + ] + }, + "type": "array", + "minItems": 1, + "title": "Guardrail points", + "description": "Where this risk is evaluated: `input` (user message), `output` (model response), or `tool` (tool/MCP content)." + }, + "violation_message": { + "type": "string", + "title": "Violation message", + "description": "Message to be displayed when this risk is violated" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "description", + "points", + "violation_message" + ], + "title": "RiskDefinition", + "description": "Definition for a custom risk category.\n\nCustom risks allow applications to add use-case-specific safety checks\nbeyond the standard harm, jailbreak, leetspeak, amnesia, and\nhistory_politics checks.\nExample:\n liability_risk = RiskDefinition(\n name=\"liability\",\n description=\"Content requesting legal, medical, or financial advice\",\n threshold=0.55,\n points=[\"input\"],\n )\n pii_risk = RiskDefinition(\n name=\"pii_request\",\n description=\"User is asking the AI to reveal personal information\",\n threshold=0.50,\n points=[\"input\", \"tool\"],\n )\nNote:\n To enable think mode (detailed reasoning) for a risk, add the risk name\n to the `thinking_enabled` list in `ModerationConfig`. Do not set\n `enable_thinking` directly - it is managed internally." + }, "RlsapiV1Attachment": { "properties": { "contents": { From 04e5b263683ad0f1736484fca06c6a99d9415dda Mon Sep 17 00:00:00 2001 From: Anik Bhattacharjee Date: Tue, 1 Sep 2026 11:18:30 -0400 Subject: [PATCH 003/120] LCORE-2985: Add OpenTelemetry instrumentation for stream interrupt endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds custom OpenTelemetry span instrumentation to `POST /v1/streaming_query/interrupt` so interrupt requests are observable in traces, including their outcome. Each interrupt request emits **one span**: `stream.interrupt`. **Span attributes** - `interrupt.request_id` — the streaming request id targeted by the interrupt - `stream.conversation.id` — conversation the stream belongs to - `interrupt.result` — cancel outcome: `cancelled` / `not_found` / `forbidden` / `already_done` **Span status** The cancel outcome is reflected in the span status: - `OK` — `cancelled` and `already_done` (both are HTTP 200 responses) - `ERROR` (with reason) — `not_found` (404) and `forbidden` (403) **Supporting change: conversation id in the interrupt registry** The interrupt request carries only `request_id`, so the conversation id was not otherwise available at the endpoint. `conversation_id` is now retained on `ActiveStream` and populated at stream registration time (from the response generator context), letting the endpoint surface `stream.conversation.id` on the span. **Privacy** `stream.conversation.id` is recorded **only when the caller owns the stream**. A non-owner (forbidden) or unknown (not-found) request never has a conversation id placed on its span, so a caller cannot surface another user's conversation identifier through a trace. No tokens or secrets are recorded. Signed-off-by: Anik Bhattacharjee --- src/app/endpoints/stream_interrupt.py | 85 ++++++--- src/utils/otel_tracing.py | 3 + src/utils/stream_interrupts.py | 12 +- .../app/endpoints/test_stream_interrupt.py | 167 ++++++++++++++++++ .../telemetry/test_configuration_snapshot.py | 12 +- tests/unit/utils/test_stream_interrupts.py | 2 + 6 files changed, 246 insertions(+), 35 deletions(-) diff --git a/src/app/endpoints/stream_interrupt.py b/src/app/endpoints/stream_interrupt.py index efbe97ff1..bc53358d9 100644 --- a/src/app/endpoints/stream_interrupt.py +++ b/src/app/endpoints/stream_interrupt.py @@ -3,6 +3,8 @@ from typing import Annotated, Any from fastapi import APIRouter, Depends, HTTPException +from opentelemetry import trace +from opentelemetry.trace import Status, StatusCode from authentication import get_auth_dependency from authentication.interface import AuthTuple @@ -17,6 +19,7 @@ ) from models.api.responses.successful import StreamingInterruptResponse from models.config import Action +from utils.otel_tracing import SpanAttributes from utils.stream_interrupts import ( CancelStreamResult, StreamInterruptRegistry, @@ -24,6 +27,7 @@ ) router = APIRouter(tags=["streaming_query_interrupt"]) +tracer = trace.get_tracer(__name__) stream_interrupt_responses: dict[int | str, dict[str, Any]] = { 200: StreamingInterruptResponse.openapi_response(), @@ -62,31 +66,62 @@ async def stream_interrupt_endpoint_handler( """ user_id, _, _, _ = auth request_id = interrupt_request.request_id - cancel_result = registry.cancel_stream(request_id, user_id) - if cancel_result == CancelStreamResult.NOT_FOUND: - response = NotFoundResponse( - resource="streaming request", - resource_id=request_id, - ) - raise HTTPException(**response.model_dump()) - if cancel_result == CancelStreamResult.FORBIDDEN: - response = ForbiddenResponse( - response="User does not have permission to interrupt this streaming request", - cause=( - f"User {user_id} does not own streaming request " - f"with ID {request_id}" - ), - ) - raise HTTPException(**response.model_dump()) - if cancel_result == CancelStreamResult.ALREADY_DONE: + + with tracer.start_as_current_span("stream.interrupt") as span: + span.set_attribute(SpanAttributes.INTERRUPT_REQUEST_ID, request_id) + + # Surface the conversation id when the caller owns the stream. The + # conversation id is looked up before cancellation so it is available + # for the span regardless of the cancel outcome. It is only recorded + # for streams owned by the requesting user to avoid exposing another + # user's conversation identifier. + existing_stream = registry.get_stream(request_id) + if ( + existing_stream is not None + and existing_stream.user_id == user_id + and existing_stream.conversation_id + ): + span.set_attribute( + SpanAttributes.STREAM_CONVERSATION_ID, + existing_stream.conversation_id, + ) + + cancel_result = registry.cancel_stream(request_id, user_id) + span.set_attribute(SpanAttributes.INTERRUPT_RESULT, cancel_result.value) + + if cancel_result == CancelStreamResult.NOT_FOUND: + span.set_status(Status(StatusCode.ERROR, "streaming request not found")) + response = NotFoundResponse( + resource="streaming request", + resource_id=request_id, + ) + raise HTTPException(**response.model_dump()) + if cancel_result == CancelStreamResult.FORBIDDEN: + span.set_status( + Status(StatusCode.ERROR, "caller does not own streaming request") + ) + response = ForbiddenResponse( + response=( + "User does not have permission to interrupt this " + "streaming request" + ), + cause=( + f"User {user_id} does not own streaming request " + f"with ID {request_id}" + ), + ) + raise HTTPException(**response.model_dump()) + if cancel_result == CancelStreamResult.ALREADY_DONE: + span.set_status(Status(StatusCode.OK)) + return StreamingInterruptResponse( + request_id=request_id, + interrupted=False, + message="Streaming request already completed; nothing to interrupt", + ) + + span.set_status(Status(StatusCode.OK)) return StreamingInterruptResponse( request_id=request_id, - interrupted=False, - message="Streaming request already completed; nothing to interrupt", + interrupted=True, + message="Streaming request interrupted", ) - - return StreamingInterruptResponse( - request_id=request_id, - interrupted=True, - message="Streaming request interrupted", - ) diff --git a/src/utils/otel_tracing.py b/src/utils/otel_tracing.py index f24b35311..9b4d887c4 100644 --- a/src/utils/otel_tracing.py +++ b/src/utils/otel_tracing.py @@ -52,6 +52,9 @@ class SpanAttributes(StrEnum): FEEDBACK_CATEGORIES = "feedback.categories" FEEDBACK_STATUS_CODE = "feedback.status.code" FEEDBACK_STORAGE_OUTCOME = "feedback.storage.outcome" + INTERRUPT_REQUEST_ID = "interrupt.request_id" + STREAM_CONVERSATION_ID = "stream.conversation.id" + INTERRUPT_RESULT = "interrupt.result" MCP_SERVER_NAME = "mcp.server.name" MCP_SERVER_PROVIDER_ID = "mcp.server.provider_id" MCP_SERVERS_COUNT = "mcp.servers.count" diff --git a/src/utils/stream_interrupts.py b/src/utils/stream_interrupts.py index 8ad40650c..3d515e2e8 100644 --- a/src/utils/stream_interrupts.py +++ b/src/utils/stream_interrupts.py @@ -41,6 +41,8 @@ class ActiveStream: on_interrupt: Optional async callback invoked when the stream is cancelled, scheduled as a separate task so it runs regardless of where the ``CancelledError`` lands. + conversation_id: Conversation the streaming request belongs to, + surfaced for observability of interrupt requests. """ user_id: str @@ -48,6 +50,7 @@ class ActiveStream: on_interrupt: Optional[Callable[[], Coroutine[Any, Any, None]]] = field( default=None, repr=False ) + conversation_id: Optional[str] = None class CancelStreamResult(str, Enum): @@ -73,6 +76,7 @@ def register_stream( user_id: str, task: asyncio.Task[None], on_interrupt: Optional[Callable[[], Coroutine[Any, Any, None]]] = None, + conversation_id: Optional[str] = None, ) -> None: """Register an active stream task for interrupt support. @@ -83,10 +87,15 @@ def register_stream( task: Asyncio task associated with the stream. on_interrupt: Optional async callback to run when the stream is cancelled, executed in a separate task. + conversation_id: Conversation the stream belongs to, retained + for observability of subsequent interrupt requests. """ with self._lock: self._streams[request_id] = ActiveStream( - user_id=user_id, task=task, on_interrupt=on_interrupt + user_id=user_id, + task=task, + on_interrupt=on_interrupt, + conversation_id=conversation_id, ) def cancel_stream(self, request_id: str, user_id: str) -> CancelStreamResult: @@ -384,6 +393,7 @@ async def _on_interrupt() -> None: user_id=context.user_id, task=current_task, on_interrupt=_on_interrupt, + conversation_id=context.conversation_id, ) else: logger.warning( diff --git a/tests/unit/app/endpoints/test_stream_interrupt.py b/tests/unit/app/endpoints/test_stream_interrupt.py index d257f56f4..182edc60c 100644 --- a/tests/unit/app/endpoints/test_stream_interrupt.py +++ b/tests/unit/app/endpoints/test_stream_interrupt.py @@ -3,9 +3,15 @@ import asyncio import threading from collections.abc import Generator +from typing import Any import pytest from fastapi import HTTPException +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) +from opentelemetry.trace import StatusCode +from pytest_mock import MockerFixture from app.endpoints.stream_interrupt import stream_interrupt_endpoint_handler from models.api.requests import StreamingInterruptRequest @@ -31,6 +37,8 @@ REQUEST_ID_ALREADY_COMPLETED, ) +CONVERSATION_ID = "323e4567-e89b-12d3-a456-426614174777" + @pytest.fixture(name="registry") def registry_fixture() -> Generator[StreamInterruptRegistry, None, None]: @@ -225,3 +233,162 @@ async def pending_stream() -> None: registry=registry, ) assert second_response.interrupted is False + + +class TestStreamInterruptOtelSpans: + """OTEL instrumentation tests for the stream interrupt endpoint.""" + + @pytest.mark.asyncio + async def test_interrupt_success_emits_span_with_attributes( + self, + mocker: MockerFixture, + registry: StreamInterruptRegistry, + otel: tuple[Any, InMemorySpanExporter], + ) -> None: + """A successful interrupt emits one span with the expected attributes.""" + tracer, exporter = otel + mocker.patch("app.endpoints.stream_interrupt.tracer", tracer) + + async def pending_stream() -> None: + await asyncio.sleep(10) + + task = asyncio.create_task(pending_stream()) + registry.register_stream( + REQUEST_ID_SUCCESS, + OWNER_USER_ID, + task, + conversation_id=CONVERSATION_ID, + ) + + await stream_interrupt_endpoint_handler( + interrupt_request=StreamingInterruptRequest(request_id=REQUEST_ID_SUCCESS), + auth=(OWNER_USER_ID, "mock_username", False, "mock_token"), + registry=registry, + ) + + spans = exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + assert span.name == "stream.interrupt" + + attrs = dict(span.attributes or {}) + assert attrs["interrupt.request_id"] == REQUEST_ID_SUCCESS + assert attrs["stream.conversation.id"] == CONVERSATION_ID + assert attrs["interrupt.result"] == CancelStreamResult.CANCELLED.value + assert span.status.status_code == StatusCode.OK + + with pytest.raises(asyncio.CancelledError): + await task + + @pytest.mark.asyncio + async def test_interrupt_not_found_sets_error_status( + self, + mocker: MockerFixture, + registry: StreamInterruptRegistry, + otel: tuple[Any, InMemorySpanExporter], + ) -> None: + """An unknown request id records the not_found result and error status.""" + tracer, exporter = otel + mocker.patch("app.endpoints.stream_interrupt.tracer", tracer) + + with pytest.raises(HTTPException): + await stream_interrupt_endpoint_handler( + interrupt_request=StreamingInterruptRequest( + request_id=REQUEST_ID_NOT_FOUND + ), + auth=(OWNER_USER_ID, "mock_username", False, "mock_token"), + registry=registry, + ) + + spans = exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + attrs = dict(span.attributes or {}) + assert attrs["interrupt.request_id"] == REQUEST_ID_NOT_FOUND + assert attrs["interrupt.result"] == CancelStreamResult.NOT_FOUND.value + # No conversation id is recorded when the stream is unknown. + assert "stream.conversation.id" not in attrs + assert span.status.status_code == StatusCode.ERROR + + @pytest.mark.asyncio + async def test_interrupt_wrong_user_sets_error_and_hides_conversation( + self, + mocker: MockerFixture, + registry: StreamInterruptRegistry, + otel: tuple[Any, InMemorySpanExporter], + ) -> None: + """A non-owner interrupt records forbidden and does not leak conversation id.""" + tracer, exporter = otel + mocker.patch("app.endpoints.stream_interrupt.tracer", tracer) + + async def pending_stream() -> None: + await asyncio.sleep(10) + + task = asyncio.create_task(pending_stream()) + registry.register_stream( + request_id=REQUEST_ID_WRONG_USER, + user_id=OWNER_USER_ID, + task=task, + conversation_id=CONVERSATION_ID, + ) + + with pytest.raises(HTTPException): + await stream_interrupt_endpoint_handler( + interrupt_request=StreamingInterruptRequest( + request_id=REQUEST_ID_WRONG_USER + ), + auth=(NON_OWNER_USER_ID, "mock_username", False, "mock_token"), + registry=registry, + ) + + spans = exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + attrs = dict(span.attributes or {}) + assert attrs["interrupt.result"] == CancelStreamResult.FORBIDDEN.value + # A non-owner must not see the stream's conversation id. + assert "stream.conversation.id" not in attrs + assert span.status.status_code == StatusCode.ERROR + + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + + @pytest.mark.asyncio + async def test_interrupt_already_done_records_ok_status( + self, + mocker: MockerFixture, + registry: StreamInterruptRegistry, + otel: tuple[Any, InMemorySpanExporter], + ) -> None: + """An already-completed stream records already_done with an OK status.""" + tracer, exporter = otel + mocker.patch("app.endpoints.stream_interrupt.tracer", tracer) + + async def completed_stream() -> None: + return None + + task = asyncio.create_task(completed_stream()) + await task + registry.register_stream( + REQUEST_ID_ALREADY_COMPLETED, + OWNER_USER_ID, + task, + conversation_id=CONVERSATION_ID, + ) + + await stream_interrupt_endpoint_handler( + interrupt_request=StreamingInterruptRequest( + request_id=REQUEST_ID_ALREADY_COMPLETED + ), + auth=(OWNER_USER_ID, "mock_username", False, "mock_token"), + registry=registry, + ) + + spans = exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + attrs = dict(span.attributes or {}) + assert attrs["interrupt.result"] == CancelStreamResult.ALREADY_DONE.value + assert attrs["stream.conversation.id"] == CONVERSATION_ID + assert span.status.status_code == StatusCode.OK diff --git a/tests/unit/telemetry/test_configuration_snapshot.py b/tests/unit/telemetry/test_configuration_snapshot.py index 36c4273c6..57d7c5aae 100644 --- a/tests/unit/telemetry/test_configuration_snapshot.py +++ b/tests/unit/telemetry/test_configuration_snapshot.py @@ -1194,9 +1194,7 @@ def test_no_pii_in_lightspeed_stack_snapshot(self) -> None: ), f"PII leaked in lightspeed-stack snapshot: '{pii_value}'" @pytest.mark.asyncio - async def test_no_pii_in_ogx_snapshot( - self, ogx_config_file: str - ) -> None: + async def test_no_pii_in_ogx_snapshot(self, ogx_config_file: str) -> None: """Verify no PII leaks in OGX snapshot JSON.""" json_str = json.dumps(await build_ogx_snapshot(ogx_config_file)) for pii_value in LLAMA_STACK_PII_VALUES: @@ -1205,9 +1203,7 @@ async def test_no_pii_in_ogx_snapshot( ), f"PII leaked in llama-stack snapshot: '{pii_value}'" @pytest.mark.asyncio - async def test_no_pii_in_combined_snapshot( - self, ogx_config_file: str - ) -> None: + async def test_no_pii_in_combined_snapshot(self, ogx_config_file: str) -> None: """Verify no PII leaks in the combined snapshot JSON.""" snapshot = await build_configuration_snapshot( build_fully_populated_config(), ogx_config_file @@ -1228,9 +1224,7 @@ def test_snapshot_only_contains_allowlisted_fields(self) -> None: ), f"Snapshot contains unexpected top-level keys: {unexpected}" @pytest.mark.asyncio - async def test_provider_config_not_leaked( - self, ogx_config_file: str - ) -> None: + async def test_provider_config_not_leaked(self, ogx_config_file: str) -> None: """Verify provider config sections (with secrets) are not included.""" json_str = json.dumps(await build_ogx_snapshot(ogx_config_file)) assert "api_key" not in json_str diff --git a/tests/unit/utils/test_stream_interrupts.py b/tests/unit/utils/test_stream_interrupts.py index 4faea720d..f612ef54d 100644 --- a/tests/unit/utils/test_stream_interrupts.py +++ b/tests/unit/utils/test_stream_interrupts.py @@ -139,6 +139,7 @@ def test_register_interrupt_callback_registers_current_task( context = mocker.Mock(spec=ResponseGeneratorContext) context.request_id = "req-1" context.user_id = "user_1" + context.conversation_id = "conv-1" responses_params = mocker.Mock(spec=ResponsesApiParams) turn_summary = TurnSummary() background_tasks: list[asyncio.Task[None]] = [] @@ -157,6 +158,7 @@ async def run() -> list[bool]: registry.register_stream.assert_called_once() assert registry.register_stream.call_args.kwargs["request_id"] == "req-1" assert registry.register_stream.call_args.kwargs["user_id"] == "user_1" + assert registry.register_stream.call_args.kwargs["conversation_id"] == "conv-1" assert persist_mock.await_count == 0 on_interrupt = registry.register_stream.call_args.kwargs["on_interrupt"] From 84471193ddfb67471f1a93caa58922fc1815fe21 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Thu, 3 Sep 2026 09:17:32 +0200 Subject: [PATCH 004/120] LCORE-3296: Removed unused argument --- scripts/vulnerability_report.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/vulnerability_report.py b/scripts/vulnerability_report.py index daae6ce9d..75c002d29 100644 --- a/scripts/vulnerability_report.py +++ b/scripts/vulnerability_report.py @@ -428,9 +428,7 @@ def process_dependabot_file(dependabot_file: str) -> dict[str, Any]: return stat -def save_graph( - fig: Figure, prefix: str, postfix: str, svg_output: bool, png_output: bool -) -> None: +def save_graph(prefix: str, postfix: str, svg_output: bool, png_output: bool) -> None: """ Save a figure in SVG and/or PNG formats based on output flags. @@ -465,7 +463,7 @@ def generate_overal_state_graph( ) ax.set_ylim(top=400) ax.set_xticks(range(len(D)), list(D.keys())) - save_graph(fig, prefix, "state", svg_output, png_output) + save_graph(prefix, "state", svg_output, png_output) def generate_severity_graph( From 5228693998c7dd8aeb0e646ae2f631298d4d682f Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Thu, 3 Sep 2026 09:19:20 +0200 Subject: [PATCH 005/120] LCORE-3297: Snake-case naming --- scripts/vulnerability_report.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/vulnerability_report.py b/scripts/vulnerability_report.py index daae6ce9d..a095b9681 100644 --- a/scripts/vulnerability_report.py +++ b/scripts/vulnerability_report.py @@ -459,12 +459,12 @@ def generate_overal_state_graph( png_output (bool): Whether to save in PNG format. """ fig, ax = plt.subplots() - D = stat["state"] + data = stat["state"] ax.bar( - range(len(D)), list(D.values()), align="center", color=["#c00000", "#00c000"] + range(len(data)), list(data.values()), align="center", color=["#c00000", "#00c000"] ) ax.set_ylim(top=400) - ax.set_xticks(range(len(D)), list(D.keys())) + ax.set_xticks(range(len(data)), list(data.keys())) save_graph(fig, prefix, "state", svg_output, png_output) From 5b25cc9cacc27a4f5fee69973d77d361637cf646 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Thu, 3 Sep 2026 09:21:17 +0200 Subject: [PATCH 006/120] LCORE-3597: Added missing type annotation --- tests/unit/utils/test_otel_tracing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/utils/test_otel_tracing.py b/tests/unit/utils/test_otel_tracing.py index 2d6405b84..3148cd242 100644 --- a/tests/unit/utils/test_otel_tracing.py +++ b/tests/unit/utils/test_otel_tracing.py @@ -168,7 +168,7 @@ def test_set_single_attribute(self, otel: Generator[Any, Any, Any]) -> None: assert len(spans) == 1 assert spans[0].attributes[SpanAttributes.SESSION_ID] == "test-session-123" - def test_set_multiple_attributes(self, otel): + def test_set_multiple_attributes(self, otel: Generator[Any, Any, Any]) -> None: """Test setting multiple attributes on a span.""" tracer, exporter = otel with tracer.start_as_current_span("test_span") as span: From e496557f07e8e877ec08cb663373e576b05a5dab Mon Sep 17 00:00:00 2001 From: Andrej Simurka Date: Tue, 1 Sep 2026 13:11:35 +0200 Subject: [PATCH 007/120] LCORE-2547: address OGX rename review findings Targeted doc and CLI fixes from review: restore poc-results under ogx-config-merge with relative links, OgxConfiguration naming in published schema docs, building_applications link, migration accuracy, prompt-guardrails anchor, and stale CLI help. Co-authored-by: Cursor --- .../ogx-config-merge/ogx-config-merge-spike.md | 14 +++++++------- .../prompt-guardrails/prompt-guardrails-spike.md | 2 +- docs/devel_doc/openapi.json | 2 +- docs/devel_doc/openapi.md | 6 +++--- docs/migrations/v0.7.0.md | 9 +++++++-- docs/user_doc/config.html | 9 ++++----- docs/user_doc/config.json | 14 +++++++------- docs/user_doc/config.md | 6 +++--- src/lightspeed_stack.py | 11 +++++------ src/models/config.py | 2 +- tests/unit/utils/dumpers/test_models_dumper.py | 2 +- 11 files changed, 40 insertions(+), 37 deletions(-) diff --git a/docs/design/ogx-config-merge/ogx-config-merge-spike.md b/docs/design/ogx-config-merge/ogx-config-merge-spike.md index 0733fea1f..d0a41fc12 100644 --- a/docs/design/ogx-config-merge/ogx-config-merge-spike.md +++ b/docs/design/ogx-config-merge/ogx-config-merge-spike.md @@ -200,7 +200,7 @@ backend-specific synthesizer translates the canonical LCORE vocabulary to its target shape; we do not adopt either backend's surface verbatim. **Pydantic AI research findings** (full report: -[`pydantic-ai-research.md`](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/ogx-config-merge/poc-results/pydantic-ai-research.md), +[`pydantic-ai-research.md`](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/llama-stack-config-merge/poc-results/pydantic-ai-research.md), pass dated 2026-05-20 against `pydantic-ai 1.98.0`): - Pydantic AI's per-Agent `:` string + `Provider(...)` @@ -884,13 +884,13 @@ removed from the tree after merge (PoC validation results aren't kept on `main`); the links below are permalinks to the files at the merge commit, where they remain in history: -- [`lightspeed-stack-unified-library.yaml`](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/ogx-config-merge/poc-results/lightspeed-stack-unified-library.yaml) +- [`lightspeed-stack-unified-library.yaml`](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/llama-stack-config-merge/poc-results/lightspeed-stack-unified-library.yaml) — the unified-mode config used. -- [`library-mode/synthesized-run.yaml`](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/ogx-config-merge/poc-results/library-mode/synthesized-run.yaml) +- [`library-mode/synthesized-run.yaml`](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/llama-stack-config-merge/poc-results/library-mode/synthesized-run.yaml) — what LCORE produced (3.7 KB). -- [`library-mode/query-response.json`](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/ogx-config-merge/poc-results/library-mode/query-response.json) +- [`library-mode/query-response.json`](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/llama-stack-config-merge/poc-results/library-mode/query-response.json) — a real `/v1/query` round-trip. -- [`library-mode/README.md`](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/ogx-config-merge/poc-results/library-mode/README.md) +- [`library-mode/README.md`](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/llama-stack-config-merge/poc-results/library-mode/README.md) — walkthrough. Summary of validation: @@ -943,7 +943,7 @@ Summary of validation: query. The implementation JIRAs' e2e coverage must exercise a real Llama Guard model (e.g. `meta-llama/Llama-Guard-3-8B`) end-to-end. Caught by CodeRabbit on the PoC artifact at - [`synthesized-run.yaml` L110](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/ogx-config-merge/poc-results/library-mode/synthesized-run.yaml#L110). + [`synthesized-run.yaml` L110](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/llama-stack-config-merge/poc-results/library-mode/synthesized-run.yaml#L110). --- @@ -1175,7 +1175,7 @@ adjust it for your environment before running.) # 0. Fetch the PoC config from the merge commit (removed from the tree post-merge) mkdir -p /tmp/lcore-836-poc curl -sSL -o /tmp/lcore-836-poc/lightspeed-stack-unified-library.yaml \ - https://raw.githubusercontent.com/lightspeed-core/lightspeed-stack/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/ogx-config-merge/poc-results/lightspeed-stack-unified-library.yaml + https://raw.githubusercontent.com/lightspeed-core/lightspeed-stack/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/llama-stack-config-merge/poc-results/lightspeed-stack-unified-library.yaml # 1. Start LCORE in library mode with the unified config export OPENAI_API_KEY= diff --git a/docs/design/prompt-guardrails/prompt-guardrails-spike.md b/docs/design/prompt-guardrails/prompt-guardrails-spike.md index e7171525e..49e57c343 100644 --- a/docs/design/prompt-guardrails/prompt-guardrails-spike.md +++ b/docs/design/prompt-guardrails/prompt-guardrails-spike.md @@ -404,7 +404,7 @@ refusals. Every hard part of LCORE-230 is precisely what it does not do, and LCS's own capabilities are better fitted on all five axes (real LLM detector, refusal-string semantics, true redaction, careful multimodal handling, Pydantic config). This also matches the precedent already set -in `docs/design/ogx-config-merge/ogx-config-merge-spike.md:220` +in `docs/design/ogx-config-merge/ogx-config-merge-spike.md#decision-s5-where-do-backend-agnostic-high-level-keys-sit` ("Do not preemptively abstract `safety.*`"). **Already taken from it**: the `AsyncGuardrail` blocking/concurrent/ diff --git a/docs/devel_doc/openapi.json b/docs/devel_doc/openapi.json index b71fad22f..5f776fe29 100644 --- a/docs/devel_doc/openapi.json +++ b/docs/devel_doc/openapi.json @@ -16076,7 +16076,7 @@ "additionalProperties": false, "type": "object", "title": "OgxConfiguration", - "description": "OGX configuration.\n\nOGX is a comprehensive system that provides a uniform set of tools\nfor building, scaling, and deploying generative AI applications, enabling\ndevelopers to create, integrate, and orchestrate multiple AI services and\ncapabilities into an adaptable setup.\n\nUseful resources:\n\n - [OGX](https://ogx-ai.github.io/)\n - [Python OGX client](https://github.com/ogx-ai/ogx-client-python)\n - [Build AI Applications with OGX](https://ogx-ai.github.io/)" + "description": "OGX configuration.\n\nOGX is a comprehensive system that provides a uniform set of tools\nfor building, scaling, and deploying generative AI applications, enabling\ndevelopers to create, integrate, and orchestrate multiple AI services and\ncapabilities into an adaptable setup.\n\nUseful resources:\n\n - [OGX](https://ogx-ai.github.io/)\n - [Python OGX client](https://github.com/ogx-ai/ogx-client-python)\n - [Build AI Applications with OGX](https://ogx-ai.github.io/docs/building_applications)" }, "OkpConfiguration": { "properties": { diff --git a/docs/devel_doc/openapi.md b/docs/devel_doc/openapi.md index 7ed7626ab..c1cdba272 100644 --- a/docs/devel_doc/openapi.md +++ b/docs/devel_doc/openapi.md @@ -260,7 +260,7 @@ Lightspeed Core Stack (LCS) service API specification. * [JwtConfiguration](#jwtconfiguration) * [JwtRoleRule](#jwtrolerule) * [LivenessResponse](#livenessresponse) - * [LlamaStackConfiguration](#llamastackconfiguration) + * [OgxConfiguration](#ogxconfiguration) * [MCPClientAuthOptionsResponse](#mcpclientauthoptionsresponse) * [MCPListToolsTool](#mcplisttoolstool) * [MCPServerAuthInfo](#mcpserverauthinfo) @@ -6728,7 +6728,7 @@ Attributes: | alive | boolean | Flag indicating that the app is alive | -## LlamaStackConfiguration +## OgxConfiguration OGX configuration. @@ -6742,7 +6742,7 @@ Useful resources: - [OGX](https://ogx-ai.github.io/) - [Python OGX client](https://github.com/ogx-ai/ogx-client-python) - - [Build AI Applications with OGX](https://ogx-ai.github.io/) + - [Build AI Applications with OGX](https://ogx-ai.github.io/docs/building_applications) | Field | Type | Description | diff --git a/docs/migrations/v0.7.0.md b/docs/migrations/v0.7.0.md index fa468ecdb..202c4ca85 100644 --- a/docs/migrations/v0.7.0.md +++ b/docs/migrations/v0.7.0.md @@ -10,8 +10,9 @@ ## OGX naming (`llama_stack` → `ogx`) In v0.7.0, Lightspeed Core Stack adopts `ogx` as the canonical name for the -OGX configuration section and related API fields. The old `llama_stack` names -remain available as **deprecated aliases** with a startup warning. +OGX configuration section in `lightspeed-stack.yaml`. The deprecated +`llama_stack:` top-level YAML key remains accepted with a startup warning. +API response fields were renamed without backward-compatible aliases. ### Configuration YAML @@ -51,10 +52,14 @@ Migrate configs to `ogx:` when convenient. ### `/info` API +The `/info` response field was renamed; the old name is not returned: + | v0.6.x | v0.7.0 | |---|---| | `llama_stack_version` | `ogx_version` | +Update clients that read `/info` to use `ogx_version`. + --- ## RAG Configuration diff --git a/docs/user_doc/config.html b/docs/user_doc/config.html index d6990e9e3..f5baed350 100644 --- a/docs/user_doc/config.html +++ b/docs/user_doc/config.html @@ -1220,7 +1220,7 @@

JwtRoleRule

-

LlamaStackConfiguration

+

OgxConfiguration

OGX configuration.

OGX is a comprehensive system that provides a uniform set of tools for building, scaling, and deploying generative AI applications, @@ -1229,15 +1229,14 @@

LlamaStackConfiguration

Useful resources:

@@ -2690,7 +2689,7 @@

UnifiedInferenceProvider

-

UnifiedLlamaStackConfig

+

UnifiedOgxConfig

Backend-specific knobs for unified-mode OGX synthesis.

Per Decision S5 of the design spike, backend-agnostic high-level sections (inference, …) live at the configuration root, not here. This diff --git a/docs/user_doc/config.json b/docs/user_doc/config.json index 5d2860547..ebc9e180b 100644 --- a/docs/user_doc/config.json +++ b/docs/user_doc/config.json @@ -449,7 +449,7 @@ "title": "Service configuration" }, "ogx": { - "$ref": "`#/components/schemas/`LlamaStackConfiguration", + "$ref": "`#/components/schemas/`OgxConfiguration", "description": "This section contains OGX configuration. Lightspeed Core Stack service can call OGX in library mode or in server mode.", "title": "OGX configuration" }, @@ -1037,9 +1037,9 @@ "title": "JwtRoleRule", "type": "object" }, - "LlamaStackConfiguration": { + "OgxConfiguration": { "additionalProperties": false, - "description": "OGX configuration.\n\nOGX is a comprehensive system that provides a uniform set of tools\nfor building, scaling, and deploying generative AI applications, enabling\ndevelopers to create, integrate, and orchestrate multiple AI services and\ncapabilities into an adaptable setup.\n\nUseful resources:\n\n - [OGX](https://ogx-ai.github.io/)\n - [Python OGX client](https://github.com/ogx-ai/ogx-client-python)\n - [Build AI Applications with OGX](https://ogx-ai.github.io/)", + "description": "OGX configuration.\n\nOGX is a comprehensive system that provides a uniform set of tools\nfor building, scaling, and deploying generative AI applications, enabling\ndevelopers to create, integrate, and orchestrate multiple AI services and\ncapabilities into an adaptable setup.\n\nUseful resources:\n\n - [OGX](https://ogx-ai.github.io/)\n - [Python OGX client](https://github.com/ogx-ai/ogx-client-python)\n - [Build AI Applications with OGX](https://ogx-ai.github.io/docs/building_applications)", "properties": { "url": { "type": "string", @@ -1100,7 +1100,7 @@ "config": { "anyOf": [ { - "$ref": "`#/components/schemas/`UnifiedLlamaStackConfig" + "$ref": "`#/components/schemas/`UnifiedOgxConfig" }, { "type": "null" @@ -1111,7 +1111,7 @@ "title": "Unified OGX configuration" } }, - "title": "LlamaStackConfiguration", + "title": "OgxConfiguration", "type": "object" }, "ModelContextProtocolServer": { @@ -2247,7 +2247,7 @@ "title": "UnifiedInferenceProvider", "type": "object" }, - "UnifiedLlamaStackConfig": { + "UnifiedOgxConfig": { "additionalProperties": false, "description": "Backend-specific knobs for unified-mode OGX synthesis.\n\nPer Decision S5 of the design spike, backend-agnostic high-level sections\n(inference, ...) live at the configuration root, not here. This block holds\nonly the Llama-Stack-specific synthesis controls: which baseline to start\nfrom, an optional profile file, and a raw native_override escape hatch.\n\nAttributes:\n baseline: Synthesis starting point. \"default\" begins from LCORE's\n built-in baseline (src/data/default_run.yaml); \"empty\" begins from\n an empty dict (used by the migration tool for an exact round-trip).\n Ignored when `profile` is set.\n profile: Optional path to a user-authored run.yaml-shaped file used as\n the synthesis baseline. Relative paths resolve against the directory\n of the loaded lightspeed-stack.yaml.\n native_override: Raw OGX schema deep-merged last (maps merge\n recursively, lists and scalars replace). The escape hatch for\n anything the high-level sections do not express.", "properties": { @@ -2276,7 +2276,7 @@ "type": "object" } }, - "title": "UnifiedLlamaStackConfig", + "title": "UnifiedOgxConfig", "type": "object" }, "UserDataCollection": { diff --git a/docs/user_doc/config.md b/docs/user_doc/config.md index 4a1d38888..237c6f28c 100644 --- a/docs/user_doc/config.md +++ b/docs/user_doc/config.md @@ -432,7 +432,7 @@ Rule for extracting roles from JWT claims. | roles | array | Roles to be assigned if the rule matches | -## LlamaStackConfiguration +## OgxConfiguration OGX configuration. @@ -446,7 +446,7 @@ Useful resources: - [OGX](https://ogx-ai.github.io/) - [Python OGX client](https://github.com/ogx-ai/ogx-client-python) - - [Build AI Applications with OGX](https://ogx-ai.github.io/) + - [Build AI Applications with OGX](https://ogx-ai.github.io/docs/building_applications) | Field | Type | Description | @@ -1036,7 +1036,7 @@ Attributes: | extra | object | Additional provider-config keys merged verbatim into the synthesized provider's config block. | -## UnifiedLlamaStackConfig +## UnifiedOgxConfig Backend-specific knobs for unified-mode OGX synthesis. diff --git a/src/lightspeed_stack.py b/src/lightspeed_stack.py index 1cc871e49..01ae41e20 100644 --- a/src/lightspeed_stack.py +++ b/src/lightspeed_stack.py @@ -32,10 +32,10 @@ def create_argument_parser() -> ArgumentParser: error_responses,common,agents,common_responses} dump schemas for selected models group into OpenAPI-compatible file and quit - -c / --config: path to the configuration file (default "lightspeed-stack.yaml") - - -g / --generate-ogx-configuration: generate an OGX - configuration from the service configuration - - -i / --input-config-file: OGX input configuration filename (default "run.yaml") - - -o / --output-config-file: OGX output configuration filename (default "run_.yaml") + - --synthesized-config-output: path for synthesized OGX run.yaml in unified library mode + - --migrate-config: migrate legacy two-file config to unified single file and exit + - --run-yaml: legacy OGX run.yaml path (with --migrate-config) + - --migrate-output: unified config output path (with --migrate-config) Returns: Configured ArgumentParser for parsing the service CLI options. @@ -154,8 +154,7 @@ def main() -> None: the quota scheduler, and starts the Uvicorn web service. Raises: - SystemExit: when configuration dumping or OGX generation fails - (exits with status 1). + SystemExit: when configuration dumping or migration fails (exits with status 1). """ logger.info("Lightspeed Core Stack startup") parser = create_argument_parser() diff --git a/src/models/config.py b/src/models/config.py index 06dca79df..5f7726d8a 100644 --- a/src/models/config.py +++ b/src/models/config.py @@ -854,7 +854,7 @@ class OgxConfiguration(ConfigurationBase): - [OGX](https://ogx-ai.github.io/) - [Python OGX client](https://github.com/ogx-ai/ogx-client-python) - - [Build AI Applications with OGX](https://ogx-ai.github.io/) + - [Build AI Applications with OGX](https://ogx-ai.github.io/docs/building_applications) """ url: Optional[AnyHttpUrl] = Field( diff --git a/tests/unit/utils/dumpers/test_models_dumper.py b/tests/unit/utils/dumpers/test_models_dumper.py index 277e2a721..6d4834cd6 100644 --- a/tests/unit/utils/dumpers/test_models_dumper.py +++ b/tests/unit/utils/dumpers/test_models_dumper.py @@ -2887,7 +2887,7 @@ def test_dump_models(tmpdir: Path) -> None: }, "OgxConfiguration": { "additionalProperties": false, - "description": "OGX configuration.\n\nOGX is a comprehensive system that provides a uniform set of tools\nfor building, scaling, and deploying generative AI applications, enabling\ndevelopers to create, integrate, and orchestrate multiple AI services and\ncapabilities into an adaptable setup.\n\nUseful resources:\n\n - [OGX](https://ogx-ai.github.io/)\n - [Python OGX client](https://github.com/ogx-ai/ogx-client-python)\n - [Build AI Applications with OGX](https://ogx-ai.github.io/)", + "description": "OGX configuration.\n\nOGX is a comprehensive system that provides a uniform set of tools\nfor building, scaling, and deploying generative AI applications, enabling\ndevelopers to create, integrate, and orchestrate multiple AI services and\ncapabilities into an adaptable setup.\n\nUseful resources:\n\n - [OGX](https://ogx-ai.github.io/)\n - [Python OGX client](https://github.com/ogx-ai/ogx-client-python)\n - [Build AI Applications with OGX](https://ogx-ai.github.io/docs/building_applications)", "properties": { "url": { "type": "string", From 922673b8461f5f46b5a22b348a60afb3b26ff65c Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Thu, 3 Sep 2026 15:02:31 +0200 Subject: [PATCH 008/120] LCORE-3580: Updated dependencies --- uv.lock | 178 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 89 insertions(+), 89 deletions(-) diff --git a/uv.lock b/uv.lock index a601df447..4df43a434 100644 --- a/uv.lock +++ b/uv.lock @@ -169,7 +169,7 @@ wheels = [ [[package]] name = "anthropic" -version = "1.2.0" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -180,22 +180,22 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/1a/b5af41cc1fa14da277ec20ca5554dd2fcbc09b8523ac59b7a97fbb88e452/anthropic-1.2.0.tar.gz", hash = "sha256:12f8eedee7b7fb5685837b1371b7bfae1b281703f62355f4632598ec2fc53b34", size = 1137443, upload-time = "2026-08-27T20:29:12.68Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/50/463166f02179ab279edb61de1589a6f69cb3838d6a2fb6f2c92a3f8042f1/anthropic-1.3.0.tar.gz", hash = "sha256:6873492a77ede8849a161ab1bc78bc9a1e492a006d0b5bb4c57ac77845df838a", size = 1148177, upload-time = "2026-09-01T17:37:10.392Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/78/3f8b52708b03309e511990700bb8d0ec7a0c9db3d2a1e0d1c3ca417a4604/anthropic-1.2.0-py3-none-any.whl", hash = "sha256:b60642b3e3cd6b8e3e328a2d3f2863ad2b6e743f1037e42cc0143f7df99f63c6", size = 1289535, upload-time = "2026-08-27T20:29:11.01Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5d/7863a9961d320c23787c7b594956afe4e878f9c0ae2376b11a20e416791d/anthropic-1.3.0-py3-none-any.whl", hash = "sha256:e7e7dbebf9f3c84a23954ab989378af6ae10a4d1804c81e9fea4b5ced695ce75", size = 1296959, upload-time = "2026-09-01T17:37:08.525Z" }, ] [[package]] name = "anyio" -version = "4.14.2" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/9a/c15a60547004a3f3cea20296c934f827ddd7bdba225a2e7e9fcb5ec48c80/anyio-4.15.0.tar.gz", hash = "sha256:b5c620ed540725e2579c31b17bb995b3bf02c9281c9cace04c7d186380bab85e", size = 276504, upload-time = "2026-09-02T21:46:36.957Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/2b21ce5ebe4d8938a247c9b0dbb7271566ae559b01795c83ea4bb2660ed7/anyio-4.15.0-py3-none-any.whl", hash = "sha256:7ecd9937369ffce8bba0b5ccb9b3a9507b101b0ed50256aecfbab27e6c2acb99", size = 131908, upload-time = "2026-09-02T21:46:35.485Z" }, ] [[package]] @@ -218,46 +218,46 @@ wheels = [ [[package]] name = "ast-serialize" -version = "0.8.0" +version = "0.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/a9/11851c3e02a3fea2ddc9932d1fdc7d2edaeecc0d2e11bc5f2a7fde2b0934/ast_serialize-0.8.0.tar.gz", hash = "sha256:6c37c43e4004dfb42d321ddedc569dc17ff4259296f3af577c9ea46a809bc010", size = 845638, upload-time = "2026-08-07T11:29:02.152Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/11/911210c3c78923273a9211a2b6cfc4c8aa723b30dab3e1c8d19afb983b40/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:86b8a1e6d90467345356098b040150e82fbc26d24a7a202224b13dc1f6264ca0", size = 1177715, upload-time = "2026-08-07T11:28:04.654Z" }, - { url = "https://files.pythonhosted.org/packages/77/89/6282881c8587606638db153cbe21e1e0c4d1f3970dee1aa0610a1c62a026/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:39e92ff8e8cb45947fe9007174b2950e1fb098e6abd00266a13cd3bcf6675068", size = 1169347, upload-time = "2026-08-07T11:28:06.1Z" }, - { url = "https://files.pythonhosted.org/packages/97/78/a9f846a03a340ff3728c915f23338ca742742f3292700559cdb3ad999b1e/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85d8d18db5b2dfcb3b7e38a4d600ca35504c0ed8a6f75cd1c811e4ffe248a15", size = 1225916, upload-time = "2026-08-07T11:28:07.654Z" }, - { url = "https://files.pythonhosted.org/packages/c0/15/aba6ef8a988a6eceb6f0359589aac509e29ae2dba67fd9bfd5af0c3f13e7/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9830ff7e764f74d9eefb01170c61a9f0fd2c027dac5fcb72e064decd57d56371", size = 1227135, upload-time = "2026-08-07T11:28:09.504Z" }, - { url = "https://files.pythonhosted.org/packages/94/29/3f63d696ea7c5b8abadcecc3505be51bd900daaccc522ed8322fa5b05a93/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6479d9722a4cd21b578f5478074c41e6169f04811996ec881655560f703a5bba", size = 1425040, upload-time = "2026-08-07T11:28:11.044Z" }, - { url = "https://files.pythonhosted.org/packages/e2/5d/0aac338604ff59df5774d4304307898982252f325ff7cafe31d52fedcb65/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a63bed264e818cd83eec11feed0f50aa162542b91132ef58afebc857182763a5", size = 1246278, upload-time = "2026-08-07T11:28:12.519Z" }, - { url = "https://files.pythonhosted.org/packages/23/ca/9f1ef795bb724719532bd86dbec11e5b66857d3fbe9b6772baec0191a6ed/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d187197d234aa45d6cfa2b096be5f666e8cc2e7eb3722d0ab8926293cf5720c", size = 1250029, upload-time = "2026-08-07T11:28:13.896Z" }, - { url = "https://files.pythonhosted.org/packages/dc/25/5e061372d2ed953b9ba3b9c4f73de3b8e9234cda3f6c088db4686801d0e1/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:2d39a56282cfcc0d8eeea37267c754be59c98d48505c23b1dae5c6011f3813dd", size = 1243575, upload-time = "2026-08-07T11:28:15.37Z" }, - { url = "https://files.pythonhosted.org/packages/a8/c1/ae7da218053120635a4ca802366c69f707203641af95372eeb83f70dfd52/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7cc5f10386994c0f4844f1e6d6a97127e9b478660eb6dec2b257644f0acab64", size = 1294396, upload-time = "2026-08-07T11:28:16.813Z" }, - { url = "https://files.pythonhosted.org/packages/2e/89/271d1f49c5269fcddcc789ea3f25be401f6723fc1138aeda539f4d05516d/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6102f2f985c2e542be85cd857678ec9356fefa792b93cadfadd31139f5696f27", size = 1401987, upload-time = "2026-08-07T11:28:18.333Z" }, - { url = "https://files.pythonhosted.org/packages/55/be/4e7d77fcf571ac7cb5cf7115a20c36642bd7d29473b45dfaaefeb9618f90/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:3a8660fe66667b76a6e9dccd1d33e66b229fde3b308db991c041609226c005b6", size = 1502904, upload-time = "2026-08-07T11:28:20.039Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ae/ed1de2db7e019d4236fbc164ffa5ef9a6022a300a342bbf142d21b7c141e/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:e7266307e5fba39836edb79def8608887af48820508bff3c5f2941e1e04d1534", size = 1496967, upload-time = "2026-08-07T11:28:21.734Z" }, - { url = "https://files.pythonhosted.org/packages/92/89/5fea507fae5c5f18b7dc7f95e5c00956574b8c717b8fd2049c504fab0b18/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca7e6fd1ad845d1cc649dc2ecd499db2f8f46af5bf8da7b70dd858774cc038b", size = 1559041, upload-time = "2026-08-07T11:28:23.194Z" }, - { url = "https://files.pythonhosted.org/packages/42/71/478d69df21b64e064554a68134c94be304270316ca676a94e63c389a636a/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2880350b13d3eae69a0d70bc1fb6c9bfaca4dbd0e20ba8cd1aa483080b56ff06", size = 1417367, upload-time = "2026-08-07T11:28:24.601Z" }, - { url = "https://files.pythonhosted.org/packages/5e/2d/8962dc8d5b3a9dc27b36f9db199afa25264c741505469d9ec10ffbfd2ba7/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:ab0f9a59f7d63d0d441b56b9a818b273705264352d5115cfee12e940e816d958", size = 1446178, upload-time = "2026-08-07T11:28:26.152Z" }, - { url = "https://files.pythonhosted.org/packages/4f/22/14d2ad4fd1d1bcd0dc687ca268e0630069f45162496260c0efb70ee0ea72/ast_serialize-0.8.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:0485a25ef519c62e749ee3c1ad8070e591b380d67226349eb5a70b228dc1ac4a", size = 1063811, upload-time = "2026-08-07T11:28:27.864Z" }, - { url = "https://files.pythonhosted.org/packages/18/1d/84a327c0202a41aa5fdba3ade33904d6d8f3b9e6806fa83568d835395850/ast_serialize-0.8.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:bd84d60bca7079e741be4ac5dbe237751a59d7f6f9f0126b11880d63822cbe16", size = 1105518, upload-time = "2026-08-07T11:28:29.691Z" }, - { url = "https://files.pythonhosted.org/packages/8c/92/74556dec52fde85a2ad84ed159991b916241043788609c15d8b77e14570b/ast_serialize-0.8.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:057769b5921336eb2d9124f2a731b42ed05ffdac559b840dbdf6f3937cf153dc", size = 1076319, upload-time = "2026-08-07T11:28:31.282Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e3/6142e920fec6ef7bccabd8c24ed8ed99f8bdc6cb8b065e1df7c6a3b2d667/ast_serialize-0.8.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e1bd223df0f6c96b396975fa604cb33bce53d9b4a0185490be4c4a289f7c9c87", size = 1184007, upload-time = "2026-08-07T11:28:34.654Z" }, - { url = "https://files.pythonhosted.org/packages/a6/e9/6e8be8df02b35d85e2b8809f7f1cfa290bdf5882b55127a539d049482db0/ast_serialize-0.8.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ddd3b61f45c132da66c5476b281891e08c1fd87fbdabe8a6973e1622efc85f06", size = 1177588, upload-time = "2026-08-07T11:28:36.318Z" }, - { url = "https://files.pythonhosted.org/packages/8c/80/7e0fd2e2e2aba257820db4a8657c4c356844d36b914b20a4af294bcfb902/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f9caa63fad8241257ae401b5ff0a64026c6adb36b8e86cbe8782d9ea505daf6", size = 1234575, upload-time = "2026-08-07T11:28:37.772Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6a/3bae0af06f9b1bae3001c44d64215f5b567877e7aae9ffd45db11c3a7647/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3926fa117b5e65019853a2969966d11c7175af377a3425991f3fe73784412405", size = 1236015, upload-time = "2026-08-07T11:28:39.14Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c4/ce2d41a1bc22508e82618901f7e10f2a5e2f9556553fea90624daf9875e2/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:485f1113af805e9e170b95ef993ca3fbd4f89c04bab25c58b4fc632d854801ab", size = 1432808, upload-time = "2026-08-07T11:28:40.664Z" }, - { url = "https://files.pythonhosted.org/packages/1a/90/f5058f209756dd70e958b7538aaa82d25d24944baf9ec8ae6f27b06fcacc/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccebbed24f1281062d5852353c72c47502955926cfcb8345ffb3a44d87ff3d3", size = 1256251, upload-time = "2026-08-07T11:28:42.223Z" }, - { url = "https://files.pythonhosted.org/packages/bf/32/7f77ea87fa0836daab706ed5cb7f903bb25fa26a77439011aee626af11d8/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:252f883290d1cdb728eb7fe1d9a7221b88af5a329aae0bc91ddee4dafb820331", size = 1258574, upload-time = "2026-08-07T11:28:43.751Z" }, - { url = "https://files.pythonhosted.org/packages/eb/5a/75b82ad2725b5e8e8c742732f9e76c6738a292d0709e1f60d10a973730b4/ast_serialize-0.8.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:96abc072ad29db8d02194afd47d68987322622787daceae82398d7b69f3ba2e6", size = 1254075, upload-time = "2026-08-07T11:28:45.28Z" }, - { url = "https://files.pythonhosted.org/packages/4e/54/8c20ed4eea805516a3fd23dd4a721ce28c64f50f0e4b359969f60a8c97a6/ast_serialize-0.8.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9118ad3e369727060b2696fc4078f250ecffca4248ba87f537f55cea9f9dce06", size = 1301018, upload-time = "2026-08-07T11:28:46.851Z" }, - { url = "https://files.pythonhosted.org/packages/cb/5b/9f14430f12fe830b656fb38f8e2e05ee13b02a88967660bef46af0ab22a8/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f359df4bd921918af8bebd142a376c77511d7151cc8ba852760b587b5a4a54f3", size = 1409951, upload-time = "2026-08-07T11:28:48.312Z" }, - { url = "https://files.pythonhosted.org/packages/2d/3d/084882eca93c842bd4262591a071ec7f825340644035e51501208cc5a8d4/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e94f9121d13fa36cbf21314783c77d05ae3a0868decd18cf5233fdcc6de49ac8", size = 1509544, upload-time = "2026-08-07T11:28:49.847Z" }, - { url = "https://files.pythonhosted.org/packages/ce/73/ea84852096c2036c61cc0b2f97b90242207419f534dc671060ee1c8e05cb/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:54f95b486018d262bcb387a9afd96f0da74508b442762b80c769454a6fbb3ee3", size = 1505671, upload-time = "2026-08-07T11:28:51.239Z" }, - { url = "https://files.pythonhosted.org/packages/cb/88/287b9a5300c1f2f651d259f670931b63110adc265b7613c885b44c5bc53d/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c38b915511e32bc718c49dbce98ff9af36bac0ad6a604f58000cd5e3aecdba7", size = 1563685, upload-time = "2026-08-07T11:28:53.112Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f3/1bc3a79afcf0c2a8d2c37182d0d659d1545a9d7f7f6dc9cf3e63d6c17135/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a2ef9cf12f2de4f1028c42c1dd7d775255e0fb3e5bb48896c97e35ef52366fe", size = 1427977, upload-time = "2026-08-07T11:28:54.418Z" }, - { url = "https://files.pythonhosted.org/packages/5c/cd/440c798957e14e31776bfeb024d8fafe0bb1d5b89c51c2f067e69938f7b0/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f18048fe9f6dd266bd577cdec48bdcecb74faaa01fe941324435483b013ed2a", size = 1454335, upload-time = "2026-08-07T11:28:55.968Z" }, - { url = "https://files.pythonhosted.org/packages/4f/4a/587eb36dcc240a54c8660f599464516b469ecad96f0dbdb6bccbedb50745/ast_serialize-0.8.0-cp39-abi3-win32.whl", hash = "sha256:31883542dd6c94d178f5db3d32fbd69c5eb88b3a7c018e7ac8cc0c45195ddbed", size = 1068858, upload-time = "2026-08-07T11:28:57.541Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a4/3e887bbd92164e183cb6e412c6a3e9198ddd446d7fe405958293ef5ef49c/ast_serialize-0.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:861794565b06337005c1447ef23103a3d5a627d08bdc827870d00d0b28ef5f51", size = 1111839, upload-time = "2026-08-07T11:28:59Z" }, - { url = "https://files.pythonhosted.org/packages/25/6c/b400476d3ceba681ab929787edc9554f6d88fcc69435eb681b00fc0457a5/ast_serialize-0.8.0-cp39-abi3-win_arm64.whl", hash = "sha256:b2a5978662fd4db463dfb4b974d2b10ac6430b98f5333aabc7051909df3561d0", size = 1083655, upload-time = "2026-08-07T11:29:00.349Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/fd/c0/5bb6885a9608d86ee5712c0d88bc405d3a49f3e44231576e130ea2f53d34/ast_serialize-0.9.0.tar.gz", hash = "sha256:79fe8be1c934aa572940d1811d8dbe4d1b6f22291e3f16755c9b062e9ac92fb7", size = 951293, upload-time = "2026-09-02T15:50:45.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/f9/a4af1bf8b35927814c09d90c3965dbfaa75c489ba34372bffafbc2209f40/ast_serialize-0.9.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:383f56e3ae925f154632458f01b4bfcde3dd382f3ec04f5c7f6d72f76524ff48", size = 1226344, upload-time = "2026-09-02T15:49:49.79Z" }, + { url = "https://files.pythonhosted.org/packages/1d/6d/d3a95823a803c21f5c9df595a0bb93aada22e7aa22bf875fe00d89422d7f/ast_serialize-0.9.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:b9ef3d4173907bd19aa8f1683be9f06e7862e6cdf2ca6bca3633305c0df32063", size = 1207384, upload-time = "2026-09-02T15:49:51.22Z" }, + { url = "https://files.pythonhosted.org/packages/f0/97/6e7f46c8455b738609c29d1b7655307a168c4b40ce4c7a2c678c8ed9cf2e/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9482672ca8ec09f85cd050a053fb88c30c882c8e20ce7a140d8defe19c0ef2eb", size = 1273139, upload-time = "2026-09-02T15:49:52.679Z" }, + { url = "https://files.pythonhosted.org/packages/f0/6e/25b70733f061766865cb04d913dc5332037c595796b871d52ab5b569abb8/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6a568d1d489f0669a31aed90ca4845aa7f08e1b8cd5d05e1905dbdc3ae9b2b0", size = 1278242, upload-time = "2026-09-02T15:49:54.236Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f0/b7820399d9c5a0b7f07c239b6da93d2e21a1b3785137fa00e16528e414b3/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a4dd005d4095a13eb312dc712c943c7730f262b000a87df963925328a38ffdb", size = 1541009, upload-time = "2026-09-02T15:49:56.149Z" }, + { url = "https://files.pythonhosted.org/packages/08/56/5146f1d2a77516e697f6f42825df79137e43560675cb4605c467775f8b4a/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:207ac73afa1f4654840593853c130eac2591dd942434176eea33f730afb3359b", size = 1290898, upload-time = "2026-09-02T15:49:57.502Z" }, + { url = "https://files.pythonhosted.org/packages/69/c4/87cd16228796d703de795a369b90b0f57f0f017f90f55c4c5876e3513a03/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ae01129e2cc5d57a3c434d8a990019de039350310a2e1dd3c9f61311964cf25", size = 1291742, upload-time = "2026-09-02T15:49:58.982Z" }, + { url = "https://files.pythonhosted.org/packages/cc/eb/13465c297268c5170b2bb746d75f37a8fad44a94a89b593071affc1071d0/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:4c522377f383670abfe21c94edc3032cb3bd34d8fcacd280fa9556907d4edd4b", size = 1300180, upload-time = "2026-09-02T15:50:00.454Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a7/10b84c4274b2507b0ed9cc1654058ad64bcedb6ac574753d6a461bc6e204/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4cd886f6e900f5e13f758cb8ef359652e690e4f3f9c257a7269e095940534167", size = 1345857, upload-time = "2026-09-02T15:50:01.875Z" }, + { url = "https://files.pythonhosted.org/packages/c7/dc/2702182c9773a15de9aabfaf66da7cb87548a56a6bac24f0c9176a4a13c3/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:f3f0f3359cf0f22bf096b07021ffe6bf0ec88ac8a2cf7ce5f4701af973112faa", size = 1448544, upload-time = "2026-09-02T15:50:03.496Z" }, + { url = "https://files.pythonhosted.org/packages/59/b5/eeef2124c9563b9861707ef4db91f153f3bb37b3e0cca9543bb88a4e9e53/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:6c428444656cffbd32c1e76626c6eec5237b58c8fcb0b5d3df75941cd50c4f3c", size = 1551572, upload-time = "2026-09-02T15:50:04.982Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8c/81d18349f1dffdcfeb80671bd737e342c86348e183692d9d0d8f573d1385/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:54f0babed5e2a4eb86a0716ac612aff33f933e7572e5bc067adcdbe672a26321", size = 1548118, upload-time = "2026-09-02T15:50:06.522Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1f/339131b60d1b0df13d9f3470cfac70f858b5649188ea01b2e7b39caeb720/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:1b779fdaee34d19900a5ba5fd6bd4cefe225650081f9de28de256eacee5113c2", size = 1674707, upload-time = "2026-09-02T15:50:07.919Z" }, + { url = "https://files.pythonhosted.org/packages/18/0a/ca77596fa229d88f96eca180d45dbe8efa11306f8b2b4f4ee301b3fe465f/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:4db7f524eaa857fbe650cac33b9cedb5ccda14393d40f640b75dfb06aa13c98c", size = 1473618, upload-time = "2026-09-02T15:50:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/88/5c/6aebeb54dd226b480014ff4488e150aa23b1de3204e2bf3f87de27e6542a/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:d2a37795a90809da6094825e7063118b4cf723b8701b134973b4566ed8b9ea09", size = 1492025, upload-time = "2026-09-02T15:50:10.755Z" }, + { url = "https://files.pythonhosted.org/packages/75/e6/c355d470a230f778311c28b80f6d934a497d094139f07230433eea18651b/ast_serialize-0.9.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:4411d1cba9eeecb301365343a7e96813b4a44fcdb20181557867ff7e751804cf", size = 1113010, upload-time = "2026-09-02T15:50:12.337Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0d/66609ace58564727b68731293cc986c2ea1d5e6ef40e96571e7fb515f0af/ast_serialize-0.9.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:b5c3724faf780e25def89c369eb6340a15dff06ac348160c61781e2373a4cd10", size = 1146404, upload-time = "2026-09-02T15:50:13.761Z" }, + { url = "https://files.pythonhosted.org/packages/b1/fd/da28e1c85f05fb9976f247d2a3aefce68866cb2939abcbdbddd9a5e3b835/ast_serialize-0.9.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:1de0933a4c1d104d77d6e75f053f5e628b54cf8f9fea809b8250cb04cda07bd3", size = 1118328, upload-time = "2026-09-02T15:50:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/92/e4/175b0a64d6c96bc1b96598c6474ce8d1ef34e0b774bcf7183f4ce696fb10/ast_serialize-0.9.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dac690f99538d9df0d23ce0299e946add2744b007a36b480a292fe361c82553d", size = 1232635, upload-time = "2026-09-02T15:50:18.133Z" }, + { url = "https://files.pythonhosted.org/packages/28/0c/d51d8463aca43aaa833fdf1f25134d6cc1b483764896decca61306ad1f6e/ast_serialize-0.9.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:2223ead73b5a5399d39610cf9c4164ad0b2bf2025226626b87ae15226d93d3f7", size = 1219313, upload-time = "2026-09-02T15:50:19.497Z" }, + { url = "https://files.pythonhosted.org/packages/ef/19/c88bdc64f86095a9d6ab325ae422b2a5e1395cd63cd8aa539003d4d4ae1d/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b7c5f5838408fb000d76abd14e886836412b7ec7eccd028dbb5ed5819780008", size = 1279981, upload-time = "2026-09-02T15:50:20.811Z" }, + { url = "https://files.pythonhosted.org/packages/86/58/a492075826df1753896dc8e8f6ababae4016d8883b670ee3a1c34788b154/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e05701fde79affa1cc53e391867f9da3eb03fa8501f87354292796b0f8398fd", size = 1286319, upload-time = "2026-09-02T15:50:22.203Z" }, + { url = "https://files.pythonhosted.org/packages/ae/79/3f6754eaa42fd2a6c36aac066890870cd44cbe0e25f75a67b1b99a2f4d82/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d013c36eb2f2ac0cb7d4d0e79918a92ab00fbce8f1542fe47f34a46e06168f82", size = 1551547, upload-time = "2026-09-02T15:50:23.528Z" }, + { url = "https://files.pythonhosted.org/packages/b1/05/8cfb7caadfaf28febaa6b61d31d778262f87f9366eda4dd9bd07ac940b75/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19cde5c2110f7b90ab1210a599178524f6c9f34862b20ba2b9aa7832c67bb35d", size = 1302468, upload-time = "2026-09-02T15:50:24.99Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e2/750a0b136bb02ff8e4a17d65a3a78cd478ee50724704df8215797a226ba3/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1514f4a39704e2e815f9fc675fc13f19f694f212086b520110840782cf3c5295", size = 1300563, upload-time = "2026-09-02T15:50:26.354Z" }, + { url = "https://files.pythonhosted.org/packages/ab/17/4c0aa852ff1e4f2d6723e8ce827136c1e1febf2845d7941ccc45426778de/ast_serialize-0.9.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:30651ccdec6d23c49ee4711b1a1096d8dbd3be38eecf2f09fdd98a608ce7ac24", size = 1308999, upload-time = "2026-09-02T15:50:27.901Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1b/6e73d0a29aedb0db30cc68f2557acaac06cd24c9783ccb90f84f89e4ce87/ast_serialize-0.9.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d9a46caf5e3f2cd266e8638b2f4ea8bf54cf376f015f8418397b4633fdb38e9b", size = 1358191, upload-time = "2026-09-02T15:50:29.237Z" }, + { url = "https://files.pythonhosted.org/packages/dc/38/2cf5d552de99e0e9804a16fea73e54d0a7382498adddf57c0f6dc09cbc70/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:aed6e413c6c22a23c33c47a01dd2adce01d7a7ed408748e896903f47d0a1aa47", size = 1458944, upload-time = "2026-09-02T15:50:30.77Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0b/5ef87adf955b6a027f616eb7b55f55a154c35ba600e9dd2d06ad2d30e5c2/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:871fb7c5b049897ee137b67efad7fe4545ad270f7eccd970da877833f8e63aa7", size = 1563421, upload-time = "2026-09-02T15:50:32.188Z" }, + { url = "https://files.pythonhosted.org/packages/81/dd/9ced05a17feeb0f83e84010d80f5a1b7b7aa19e75f0376f4d3780803654c/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:bb378efb5537b43f38660e2e6d6e138a40885cf191d43443bb3ff7ff47e9cd9b", size = 1558536, upload-time = "2026-09-02T15:50:33.861Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8b/8ad486e44fc7081a2471055befc433dddc2e51c3a88dff141b3026f64602/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:b373beff65b01fcffca5aaad3269ae629f3a998b09efdd3635e48039008a5dec", size = 1682749, upload-time = "2026-09-02T15:50:35.257Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4c/7c282aba9cfb0b92d79fac45c04e4557d9a7f08d872e5a43577a50867e30/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:25c8d517c45cf2b1820fc2af6ac593783654818f79d05646d25d624360678a4e", size = 1482441, upload-time = "2026-09-02T15:50:37.319Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3a/e45914e8cad81b660915f3784d255460a6384183b76bfc2089fdd79ec7df/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1675dc46578298ae00936164a160997801a6ca2385913150d8d16df634296cf3", size = 1499042, upload-time = "2026-09-02T15:50:38.76Z" }, + { url = "https://files.pythonhosted.org/packages/99/09/6988921dec19c810beef53539fec2e90ae551cd93853b3303a99fe45f772/ast_serialize-0.9.0-cp39-abi3-win32.whl", hash = "sha256:20fce3885eeff05a3d6afefa845c8168016e3ea1f6fc9cdc84c8db28b863a550", size = 1116391, upload-time = "2026-09-02T15:50:40.229Z" }, + { url = "https://files.pythonhosted.org/packages/fd/eb/839598a22a1f9af56d39e188451cad93dbcb0ce6539a45ac18fb8bf123fa/ast_serialize-0.9.0-cp39-abi3-win_amd64.whl", hash = "sha256:161914666a21d48b681982146ac0fa4086ef099d91c637cf595387f5f06aa099", size = 1156055, upload-time = "2026-09-02T15:50:42.05Z" }, + { url = "https://files.pythonhosted.org/packages/0d/45/c7cd8d36d3b506bbd02db5066fae3340284781168f0d08dac25deef5f69d/ast_serialize-0.9.0-cp39-abi3-win_arm64.whl", hash = "sha256:74473258a5c55855d5306c864a5c799fbff03a0f0ea1197346b2b5cc5b4ea48a", size = 1128237, upload-time = "2026-09-02T15:50:43.496Z" }, ] [[package]] @@ -429,30 +429,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.43.85" +version = "1.43.87" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8d/db/adb561589806195a40e6f5015c884981595eed85ff43a887122165ffd3f4/boto3-1.43.85.tar.gz", hash = "sha256:113b6e1aa3f5722f90c01fc63968c269a9b1fd03ac2594fe16c56a66e6331c5f", size = 112656, upload-time = "2026-08-31T23:23:53.701Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/0c/b14374e9458030076cd22ff9381cf86d170f31b648fd901db1d88011094b/boto3-1.43.87.tar.gz", hash = "sha256:8d9521c7c292194b8ce9fb61043d52e45cdba29b5f690981f3eb5e75103ba57d", size = 112691, upload-time = "2026-09-02T19:23:13.603Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/57/a58073f8659d7673b0b629987ab4295b56f44759c1b1f79b3c87330e452a/boto3-1.43.85-py3-none-any.whl", hash = "sha256:f11bdaca18e59f53ec0529f4d6203dd1f0bb7ff165e51559d62fd863024abc9b", size = 140028, upload-time = "2026-08-31T23:23:52.275Z" }, + { url = "https://files.pythonhosted.org/packages/9c/a9/7dd7cc1a2387f3c78f66e49682957b7e7d590706dc6552d108b48125022d/boto3-1.43.87-py3-none-any.whl", hash = "sha256:671cf88a353f887774603980898bc34249d05a994e1c5e24aba4f4e1bbef0951", size = 140026, upload-time = "2026-09-02T19:23:12.044Z" }, ] [[package]] name = "botocore" -version = "1.43.85" +version = "1.43.87" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/93/a8b3acb06fcbb440704f195dbf14d12bb83c9c9d67b2e699076017f3d5c6/botocore-1.43.85.tar.gz", hash = "sha256:8fc0a3c56078c629320b021edadf7a45d289eea21a4988ada6a02277e5bbbdc0", size = 16040188, upload-time = "2026-08-31T23:23:48.929Z" } +sdist = { url = "https://files.pythonhosted.org/packages/97/c4/64ebb159810a9840c57659f3ed98439bbc5680dc9708e3b08212deea301a/botocore-1.43.87.tar.gz", hash = "sha256:928598e7275fa70385d7f694d60d59afe115a0b914cc699dbf3eb60954c23bf1", size = 16064894, upload-time = "2026-09-02T19:23:08.777Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/4b/bb053aec2a9902df4cdbf9301dd5a41dd197c1cef0c085a32f5d04eeb3e3/botocore-1.43.85-py3-none-any.whl", hash = "sha256:685510e5f4c0f321806c815a60f121a176c0969665f053c4a336209cbe62b1d5", size = 15732946, upload-time = "2026-08-31T23:23:45.927Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5c/f15aa7a5da42fc85254f0150df7d377bb965b0c130c68f9701fba88e0df4/botocore-1.43.87-py3-none-any.whl", hash = "sha256:01e1e2255a8f4b959c19b210d74a5a916d13841c0344ceee15d8a3eaa4daa892", size = 15755750, upload-time = "2026-09-02T19:23:05.937Z" }, ] [[package]] @@ -1084,15 +1084,15 @@ http = [ [[package]] name = "genai-prices" -version = "0.1.5" +version = "0.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx2" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/70/76dcc9c76b416d2df9aa4c65553f88b75d2ba4fbfeb5efb137f078844cc3/genai_prices-0.1.5.tar.gz", hash = "sha256:04c2cbf4444a3b2f5d38c3b6ab8385ea28ab924ac6f9202bde9261f599be8b45", size = 109418, upload-time = "2026-08-31T09:36:22.848Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/16/e5a507d42c0eb629b48ebe6c278f2d8c3f929bb6b28f18108bdd66d8ae12/genai_prices-0.1.6.tar.gz", hash = "sha256:802c1e4cc3ed5e70a09083b83af441a58d91f62e12768f7f1b6b26c98a33fcac", size = 111810, upload-time = "2026-09-02T14:53:54.895Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/14/28f578fa25391bb8983522f3e365e8735310f4b741e54cfed3a614f50384/genai_prices-0.1.5-py3-none-any.whl", hash = "sha256:5215706950decd833ce3527a9e1e745ad0dbdd35ce1b33ac1f7167699640b82a", size = 116330, upload-time = "2026-08-31T09:36:21.364Z" }, + { url = "https://files.pythonhosted.org/packages/48/a1/43fa2a4c5557cd977e83eecec265b0b77b726b0c7b9f2f180b46c6fdb458/genai_prices-0.1.6-py3-none-any.whl", hash = "sha256:35ac8043dbcf2958488129413bfecba7304fe12a68ad4a78c5b0d15281e82814", size = 118834, upload-time = "2026-09-02T14:53:53.758Z" }, ] [[package]] @@ -1140,7 +1140,7 @@ requests = [ [[package]] name = "google-cloud-aiplatform" -version = "2.0.1" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -1157,9 +1157,9 @@ dependencies = [ { name = "pydantic" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/86/d8b154861ab292566d90dbdea276c33ef824b02de4a3fda61f2075d016d7/google_cloud_aiplatform-2.0.1.tar.gz", hash = "sha256:46e051b980baed400c5ea4328c79a8ae25f6f44a034b63bf0041aa5cd248ea84", size = 11325585, upload-time = "2026-08-28T03:41:22.864Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/cc/0c562f5d268f07234e712ee8822dd81ec4c836559f449d9e5e91e9aa2025/google_cloud_aiplatform-2.1.0.tar.gz", hash = "sha256:964eca160d4af48a2e04b5ee476fb4d38c84388f23b4420e2c71b30151d625bd", size = 11331982, upload-time = "2026-09-01T19:11:15.526Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/3b/9d7974a7430edf56f009247c8d0d68df1a8fcfc19a27598141bb9e0359a7/google_cloud_aiplatform-2.0.1-py2.py3-none-any.whl", hash = "sha256:a72586889b1eebca0816b34e5914258abe6e76f23fbac40938b47e70103cbeee", size = 9451860, upload-time = "2026-08-28T03:41:19.276Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/10f3d4ab6333672ff43ad56255b74e672c2a59b21b7a054632d05b7dd677/google_cloud_aiplatform-2.1.0-py2.py3-none-any.whl", hash = "sha256:de5c6dace6cb81943fc6ee1fad02f7a02e9b18e50c24b267bd9627f6b9cd3d14", size = 9452898, upload-time = "2026-09-01T19:11:08.19Z" }, ] [[package]] @@ -1247,7 +1247,7 @@ wheels = [ [[package]] name = "google-genai" -version = "2.21.0" +version = "2.22.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1261,9 +1261,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/a7/a45f64f22ab9302b55fcbeb32acb6f313690a7748629b01e451aad1817a3/google_genai-2.21.0.tar.gz", hash = "sha256:0ecc11c6a5b9f5e3cc58e77ae5fead00c6719f8a1b2b654b803f514a9a6b64c0", size = 677301, upload-time = "2026-08-31T21:49:14.508Z" } +sdist = { url = "https://files.pythonhosted.org/packages/37/f1/f2f31b2a6bd826bc2cb73068df880954a026474c03a4315637d20cc13965/google_genai-2.22.0.tar.gz", hash = "sha256:9fa3b5d9ddb635005d8ab2d6206fb2b3d7204b66965bbce7de13ecd1a866ebcd", size = 684719, upload-time = "2026-09-02T18:06:02.906Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/d7/c2419dd5fedd5803ce09810e4e39561a0a13a960b3f48d2581c12e42af3e/google_genai-2.21.0-py3-none-any.whl", hash = "sha256:36b575034be46a03acd603a852e22a6359f2cdd6b26bb1d65d9b7e0cc7ab3648", size = 1080223, upload-time = "2026-08-31T21:49:12.699Z" }, + { url = "https://files.pythonhosted.org/packages/de/96/0120d214958cb54f2b9c48da9f564a0e6eae269e9dd702415fb1d0551cc7/google_genai-2.22.0-py3-none-any.whl", hash = "sha256:c514001c45470cc0a942440ae1b8215445d12bfb6c373aac94637127e1f74ec6", size = 1088792, upload-time = "2026-09-02T18:06:00.629Z" }, ] [[package]] @@ -1494,7 +1494,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.29.0" +version = "1.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -1507,9 +1507,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/64/35/42316e8f6908b6d21bc8df017cc6efba94fb5edbf99b64e28dd142325e20/huggingface_hub-1.29.0.tar.gz", hash = "sha256:6ebb385a581435325cf6d5c5b233d5d4bc91175834d99fd65dae14379b36e9ad", size = 963121, upload-time = "2026-08-27T12:18:37.432Z" } +sdist = { url = "https://files.pythonhosted.org/packages/35/97/2eb4abaa5b969ed385066a0496a3823b3ff467fc1082e2202955f1867d60/huggingface_hub-1.30.0.tar.gz", hash = "sha256:e6a6120bc8c8e2723d03648434ee247088cceb55ba7067e7d34d692cad5fdb57", size = 964291, upload-time = "2026-09-03T10:05:14.053Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/a5/47c2ea9b228ccbcba8467e9a64823146e8ebbad29855e591d8f5eedcc9c7/huggingface_hub-1.29.0-py3-none-any.whl", hash = "sha256:b00f7782afc14db4bc6572763810a635bdfbab8623d957bfb553bd18e03852cd", size = 795768, upload-time = "2026-08-27T12:18:35.431Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0e/3e45bbe0dd48f4e56b1d46649d342de853cd1c7e815323472ab62687f153/huggingface_hub-1.30.0-py3-none-any.whl", hash = "sha256:96ae0a8e99a234374a6fe43e989ebd21c04640b91ab2927e7e5773ba1131ca59", size = 796795, upload-time = "2026-09-03T10:05:12.21Z" }, ] [[package]] @@ -2989,11 +2989,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.11.6" +version = "4.11.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/1d/6e762a6b060e662208951aefc5c39f6a96a272c4a10c0c1f7b6113fc3c09/platformdirs-4.11.6.tar.gz", hash = "sha256:1a4016e373f89f8ec458431fe0e0c5c4285858ac623f3e20efdfcbc0bd862941", size = 35131, upload-time = "2026-09-01T04:41:00.6Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/b7/802a56eca9f2fac455b8bab5375a2647b0f0e14a2cd63ef077de3c4a7658/platformdirs-4.11.7.tar.gz", hash = "sha256:4f41487eeeeeb07f3a6625e61d9bc0ae6809f92d3386dbd74392fbb76108104d", size = 35127, upload-time = "2026-09-01T13:35:10.502Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/d8/2784c6eabb991b5b7494ff9e9888c74a0a72ad613c3ec5adbfcecc0724c7/platformdirs-4.11.6-py3-none-any.whl", hash = "sha256:b22d992e863bc651c26b16242041c7979db6e3286e548f9a76cc91238fac599e", size = 23938, upload-time = "2026-09-01T04:40:58.977Z" }, + { url = "https://files.pythonhosted.org/packages/27/6e/80993e10a0482f630cef528635789233224f36b1ffd11592aa15d13ff9ce/platformdirs-4.11.7-py3-none-any.whl", hash = "sha256:8a02cb259042c79d1cd0450facc2fe6dc9d303ae7901afbe33bf8ea0b188cef6", size = 23938, upload-time = "2026-09-01T13:35:09.02Z" }, ] [[package]] @@ -4229,15 +4229,15 @@ asyncio = [ [[package]] name = "sse-starlette" -version = "3.4.8" +version = "3.4.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/00/b42a44342a054d58cb1115d7c8aa9cb4290dd9442f9c1b91a4b8173dba22/sse_starlette-3.4.8.tar.gz", hash = "sha256:ed89ffbb75cbf78a5fe2f2109cd584792ee7f9dfac96f791db546df8f15f3f9c", size = 32548, upload-time = "2026-08-05T11:19:49.982Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/e1/8a41e88e825ea26c44333897c7ffe35fe60153a2cfc097a5bd1d209ad281/sse_starlette-3.4.10.tar.gz", hash = "sha256:c6c87280d8feb4e55a8d79633782766b9cac6a26da5c79a145d00aa404117a86", size = 33720, upload-time = "2026-09-03T09:36:24.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/3a/764912c58293d95b6dcdf4cc255f9d10de310580ced547b082eb9d72018c/sse_starlette-3.4.8-py3-none-any.whl", hash = "sha256:6e82314c786709a3cd9520f2285cf9fff90e181e598e8a357b0cf80f66afba0d", size = 16516, upload-time = "2026-08-05T11:19:48.748Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3c/96018a51c7301a64f7b0579d9ce8f9b69dd39ca8ed5aa100ba3feadee503/sse_starlette-3.4.10-py3-none-any.whl", hash = "sha256:710f5f5b0527409903a22a91699db02f76f4c2eb9204e882e4ee7cada76bdf75", size = 17120, upload-time = "2026-09-03T09:36:22.56Z" }, ] [[package]] @@ -4355,29 +4355,29 @@ wheels = [ [[package]] name = "tokenizers" -version = "0.23.1" +version = "0.23.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c1/60/21f715d9faba5f5407ff759472ade058ec4a507ad62bcea47cb847239a73/tokenizers-0.23.1.tar.gz", hash = "sha256:1feeeadf865a7915adc25445dea30e9933e593c31bb96c277cee36de227c8bfa", size = 365748, upload-time = "2026-04-27T14:43:25.606Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/39/b87a87d5bb9470610b80a2d31df42fcffeaf35118b8b97952b2aff598cc7/tokenizers-0.23.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e03d6ffcbe0d56ee9c1ccd070e70a13fa750727c0277e138152acbc0252c2224", size = 3146732, upload-time = "2026-04-27T14:43:15.427Z" }, - { url = "https://files.pythonhosted.org/packages/e2/6a/068ed9f6e444c9d7e9d55ce134181325700f3d7f30410721bdc8f848d727/tokenizers-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0948bbb1ac1d7cdfc9fb6d62c596e3b7550036ad60ecd654a66ad273326324e", size = 3054954, upload-time = "2026-04-27T14:43:13.745Z" }, - { url = "https://files.pythonhosted.org/packages/6c/36/e006edf031154cba92b8416057d92c3abe3635e4c4b0aa0b5b9bb39dde70/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf13402aff9bc533c89cb849ec3b412dc3fbeacc9744840e423d7bf3f7dc0e3", size = 3374081, upload-time = "2026-04-27T14:43:01.241Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ef/7735d226f9c7f874a6bee5e3f27fb25ecabdf207d37b8cf45286d0795893/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f836ca703b89ae07919a309f9651f7a88fd5a33d5f718ba5ad0870ec0256bad6", size = 3247641, upload-time = "2026-04-27T14:43:03.856Z" }, - { url = "https://files.pythonhosted.org/packages/b9/d9/24827036f6e21297bfffda0768e58eb6096a4f411e932964a01707857931/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae848657742035523fdf261773630cb819a26995fcd3d9ecae0c1daf6e5a4959", size = 3585624, upload-time = "2026-04-27T14:43:10.664Z" }, - { url = "https://files.pythonhosted.org/packages/0c/9a/22f3582b3a4f49358293a5206e25317621ee4526bfe9cdaa0f07a12e770e/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53b09e85775d5187941e7bab30e941b4134ab4a7dd8c68e783d231fb7ca27c51", size = 3844062, upload-time = "2026-04-27T14:43:05.643Z" }, - { url = "https://files.pythonhosted.org/packages/7e/65/b8f8814eef95800f20721384136d9a1d22241d50b2874357cb70542c392f/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea5a0ce170074329faaa8ea3f6400ecde604b6678192688533af80980daae71a", size = 3460098, upload-time = "2026-04-27T14:43:08.854Z" }, - { url = "https://files.pythonhosted.org/packages/0d/d5/1353e5f677ec27c2494fb6a6725e82d56c985f53e90ec511369e7e4f02c6/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b405006415ea148a992d093699c66eb01952bf59f4d5727089a98bda45a4", size = 3346235, upload-time = "2026-04-27T14:43:12.377Z" }, - { url = "https://files.pythonhosted.org/packages/71/89/39b6b8fc073fb6d413d0147aa333dc7eff7be65639ac9d19930a0b21bf33/tokenizers-0.23.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:56f3a77de629917652f876294dc9fe6bad4a0c43bc229dc72e59bb23a0f4729a", size = 3426398, upload-time = "2026-04-27T14:43:07.264Z" }, - { url = "https://files.pythonhosted.org/packages/0f/80/127c854da64827e5b79264ce524993a90dddcb320e5cd42412c5c02f9e8a/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d10a6d957ef01896dc274e890eee27d41bd0e74ef31e60616f0fc311345184e", size = 9823279, upload-time = "2026-04-27T14:43:17.222Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ba/44c2502feb1a058f096ddfb4e0996ef3225a01a388e1a9b094e91689fe93/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1974288a609c343774f1b897c8b482c791ab17b75ab5c8c2b1737565c1d82288", size = 9644986, upload-time = "2026-04-27T14:43:19.45Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c1/464019a9fb059870bfe4eebb4ba12208f3042035e258bf5e782906bd3847/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:120468fb4c24faf0543c835a4fabafa4deb3f20a035c9b6e83d0b553a97615d4", size = 9976181, upload-time = "2026-04-27T14:43:21.463Z" }, - { url = "https://files.pythonhosted.org/packages/79/94/3ac1432bda31626071e9b6a12709b97ae05131c804b94c8f3ac622c5da32/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3d8f40ea6268047de7046906326abed5134f27d4e8447b23763afe5808c8a96", size = 10113853, upload-time = "2026-04-27T14:43:23.617Z" }, - { url = "https://files.pythonhosted.org/packages/6a/dd/631b21433c771b1382535326f0eca80b9c9cee2e64961dd993bc9ac4669e/tokenizers-0.23.1-cp310-abi3-win32.whl", hash = "sha256:93120a930b919416da7cd10a2f606ac9919cc69cacae7980fa2140e277660948", size = 2536263, upload-time = "2026-04-27T14:43:29.888Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/2553f72aaf65a2797d4229e37fa7fbe38ffbf3e32912d31bdd78b3323e59/tokenizers-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:e7bfaf995c1bdbbd21d13539decb6650967013759318627d85daeb7881af16b7", size = 2798223, upload-time = "2026-04-27T14:43:28.51Z" }, - { url = "https://files.pythonhosted.org/packages/cd/2b/2be299bab55fc595e3d38567edb1a87f86e594842968fa9515a07bdcf422/tokenizers-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9", size = 2664127, upload-time = "2026-04-27T14:43:26.949Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/18/1e/bc6587c5ab643b2e17776cace9070a2ae73549c86bffac9934a600bf3c31/tokenizers-0.23.2.tar.gz", hash = "sha256:7f0f085686b9de0d0079e6f874ae053600db64c5d13049e0bbc0119926d25aac", size = 385745, upload-time = "2026-09-03T08:55:42.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/ed/8a443528baa6fac8dfe8c3b75b038c63ac92bb539bcabe311e227c718173/tokenizers-0.23.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:85a9a357a3764aecc904ee76bdaf8cf1ad8e5a67a1b929a487c4a39b49ed0e90", size = 3148852, upload-time = "2026-09-03T08:55:30.874Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/22da045a91732384d3a3771816bf188dc5a1f702c32e635afa7c679c0bef/tokenizers-0.23.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:986670e43691469dcee610ea0f846f91a8f84e91fc6f7a48d4c064414c0ec2bf", size = 3101593, upload-time = "2026-09-03T08:55:28.587Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4d/8f569ed49372a3ed8e57099bd515055fd48d7c95912c4307cda6973c2168/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a37039b5dfc4af84eb3ef0a92f4307e28936c8f9adccba2629d36f652e9bf7a2", size = 3516830, upload-time = "2026-09-03T08:55:14.741Z" }, + { url = "https://files.pythonhosted.org/packages/2a/de/e2f14c8919d5bf51874051d00d6c7b7e0e8bde6c6a2dbeddda7f642896ff/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b7e37ba198f24150f523e1242e83c4970de4a525480586be5dcc24d9add32c5", size = 3407975, upload-time = "2026-09-03T08:55:16.842Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bd/93c69152d02ef06ce47aed8b2bf4952dcf733c935a62791873932b2934d9/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43e4f2071e3cc8d5d86421c874aebc82659bb51a68bcdef5a0da75ee89511ccb", size = 3748165, upload-time = "2026-09-03T08:55:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b7/56b84b80bc96942bba8eb23751a9e8a1fce4faaf4390425e7083f721c98c/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:325fee2e0418a9dc6c9ecf736a5f5f0db7875183ace9549ae339da76f7a1fbb7", size = 4024165, upload-time = "2026-09-03T08:55:18.806Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8a/0175e216f005c2fe08238292663aa41e4c802b216e71047a69a0e9fc6fa3/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:950d7c9426fa72406a0ffeacdbc0bb9985f5db20eb8b263f29c79aaf83105703", size = 3591899, upload-time = "2026-09-03T08:55:22.752Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ca/ca6b93c7820df123b2662a9469e8facc826ccc94e98fdd0d615f6431e73a/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41c2f84d172449b4dadb9cdc508e3e364076613c35b16e76ecfe47a60d1e3305", size = 3386843, upload-time = "2026-09-03T08:55:26.584Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a4/4f9106d317b14a80aefea9f0e3a8d07ef25f856a7607eb7f5ab894281fcb/tokenizers-0.23.2-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:12f0835dc2ee694746a76adf7b1567d4346a4a502ebe93fb1f5f80ea49799b78", size = 3577314, upload-time = "2026-09-03T08:55:20.825Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6a/1552b70fb0d9ab074fd3fc961435d01364e79c9058481822c3af6e8d402c/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:eb2f9c8a24da020ea8c11a01a19c1c2547912d92121ae4a01cfbca46125dee40", size = 9967367, upload-time = "2026-09-03T08:55:33.188Z" }, + { url = "https://files.pythonhosted.org/packages/06/01/3ccb3a956c7528b2507b8a9714155c4baf86af593039db6ea375dd0c96c3/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f486f402f6f9abee5bb032553736813af0c710a86b2e0ca592634c55cea1f835", size = 9811886, upload-time = "2026-09-03T08:55:35.642Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/7038e612d48bda1599457f712f6bd3854eae1a9dc9c13aa47f835349db48/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:bef235815a067b2648caf6dcc7a71091b0b0fff9ee8057f6451eb9335fae52ef", size = 10146224, upload-time = "2026-09-03T08:55:38.391Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d8/8e9e4e0b287a338d8f88976729628c9d22e8a54cfaf9777018a7f7cb58a0/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5c56bda1511921587789163e524d196ed8284174ac23abd7685d5ea8da6c4718", size = 10256304, upload-time = "2026-09-03T08:55:40.977Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1f/c79a01f671a49728ebb0b61f7ff9ea45663b66cab40bc0858e9859b25c16/tokenizers-0.23.2-cp310-abi3-win32.whl", hash = "sha256:debf978920d93ba9c219bd67cc4bbfaf912c9039e41e7a28b91ec15e3728c95a", size = 2592809, upload-time = "2026-09-03T08:55:48.02Z" }, + { url = "https://files.pythonhosted.org/packages/db/f7/0a69ac6b82dbccf3f71add938a161c497952749294b8dd6dfe03a819dc40/tokenizers-0.23.2-cp310-abi3-win_amd64.whl", hash = "sha256:2e96f5699d5249c9c64aa8412e044f727aae3a4098cf830f9901ec1afc361cde", size = 2863236, upload-time = "2026-09-03T08:55:46.193Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b0/dee84cb44175be1b4c35bd2f770727494e78f0bb38e571a623ade94dbebb/tokenizers-0.23.2-cp310-abi3-win_arm64.whl", hash = "sha256:e49c394456dd9985787fec76132438ba3fb8911f857b1bf3d40119f9292d41aa", size = 2729352, upload-time = "2026-09-03T08:55:44.345Z" }, ] [[package]] From 9902fbb65bff6370fe515a5ba26c1de4c1d9ee64 Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Fri, 4 Sep 2026 19:42:39 +0200 Subject: [PATCH 009/120] LCORE-3395: point lint-openapi at docs/devel_doc/openapi.json The generated schema moved to docs/devel_doc/openapi.json (the generate target and the openapi_spectral workflow already use that path), but the lint-openapi target still linted docs/openapi.json, which no longer exists. make verify therefore failed locally on a missing file while CI, which runs Spectral directly against the new path, stayed green. --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 3a1cd4f8a..2fa421ada 100644 --- a/Makefile +++ b/Makefile @@ -339,9 +339,9 @@ docstyle: ## Check the docstring style using Docstyle checker ruff: ## Check source code using Ruff linter uv run ruff check src tests --per-file-ignores=tests/*:S101 --per-file-ignores=scripts/*:S101 -lint-openapi: ## Lint docs/openapi.json (Spectral OAS ruleset; fail on error) +lint-openapi: ## Lint docs/devel_doc/openapi.json (Spectral OAS ruleset; fail on error) @if command -v npx >/dev/null 2>&1; then \ - npx --yes @stoplight/spectral-cli@6 lint docs/openapi.json --fail-severity error --display-only-failures; \ + npx --yes @stoplight/spectral-cli@6 lint docs/devel_doc/openapi.json --fail-severity error --display-only-failures; \ else \ echo "lint-openapi: skipping Spectral (npx not found). Install Node.js for OpenAPI lint locally; CI still runs it."; \ fi From e2573e88cc4b5ae9054c087405c3e6d394830b06 Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Fri, 4 Sep 2026 19:43:11 +0200 Subject: [PATCH 010/120] LCORE-3963: keep link targets, smart links, mentions and tables in fetch-jira output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit extract_text rendered only strong/code marks, lists, headings and code blocks; every other ADF node was dropped on the floor. Link marks lost their href, inlineCard nodes (the smart links Jira makes out of pasted Jira/GitHub URLs) produced nothing at all, mentions vanished, and table rows vanished — so for the tickets that reference other tickets or PRs, the tool hid exactly the references a reader needs. Render link marks as "text " when the text is not already the URL, inlineCard as "", mentions as their display text, tables as pipe-separated rows, blockquotes with a "> " prefix and rules as "---". The Python sits inside a double-quoted shell string, so the new code sticks to single quotes and string concatenation. Verified on LCORE-3788, whose description carries five smart links that previously did not appear. --- dev-tools/fetch-jira.sh | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/dev-tools/fetch-jira.sh b/dev-tools/fetch-jira.sh index af1790246..549045e80 100755 --- a/dev-tools/fetch-jira.sh +++ b/dev-tools/fetch-jira.sh @@ -147,9 +147,22 @@ def extract_text(node, depth=0): text = f'**{text}**' elif m.get('type') == 'code': text = f'\`{text}\`' + elif m.get('type') == 'link': + # Keep the target: a link whose text differs from its + # href (ticket keys, "here", PR titles) is otherwise lost. + href = m.get('attrs', {}).get('href', '') + if href and href != text: + text = text + ' <' + href + '>' return [text] + if ntype == 'inlineCard': + # Smart links (pasted Jira/GitHub URLs) carry the URL only here. + return ['<' + node.get('attrs', {}).get('url', '') + '>'] + if ntype == 'mention': + return [node.get('attrs', {}).get('text', '@?')] if ntype == 'hardBreak': return ['\n'] + if ntype == 'rule': + return ['---'] if ntype == 'listItem': child_text = [] for c in node.get('content', []): @@ -170,6 +183,22 @@ def extract_text(node, depth=0): for c in node.get('content', []): child_text.extend(extract_text(c, depth)) return ['\`\`\`\n' + ''.join(child_text) + '\n\`\`\`'] + if ntype == 'blockquote': + child_text = [] + for c in node.get('content', []): + child_text.extend(extract_text(c, depth)) + return ['> ' + l for l in ''.join(child_text).strip().split('\n')] + if ntype == 'table': + rows = [] + for row in node.get('content', []): + cells = [] + for cell in row.get('content', []): + cell_text = [] + for c in cell.get('content', []): + cell_text.extend(extract_text(c, depth)) + cells.append(''.join(cell_text).strip()) + rows.append(' | '.join(cells)) + return rows for c in node.get('content', []): lines.extend(extract_text(c, depth)) if ntype == 'paragraph' and lines: From 271cb1a3b88162b7046313852a13e73daaa85f8c Mon Sep 17 00:00:00 2001 From: "red-hat-konflux-kflux-prd-rh02[bot]" <190377777+red-hat-konflux-kflux-prd-rh02[bot]@users.noreply.github.com> Date: Sat, 5 Sep 2026 08:04:10 +0000 Subject: [PATCH 011/120] Update Konflux references Signed-off-by: red-hat-konflux-kflux-prd-rh02 <190377777+red-hat-konflux-kflux-prd-rh02[bot]@users.noreply.github.com> --- .../lightspeed-stack-0-8-pull-request.yaml | 30 +++++++++---------- .tekton/lightspeed-stack-0-8-push.yaml | 30 +++++++++---------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.tekton/lightspeed-stack-0-8-pull-request.yaml b/.tekton/lightspeed-stack-0-8-pull-request.yaml index 665ea1ddd..aca9e80b0 100644 --- a/.tekton/lightspeed-stack-0-8-pull-request.yaml +++ b/.tekton/lightspeed-stack-0-8-pull-request.yaml @@ -193,7 +193,7 @@ spec: - name: name value: init - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-init:0.4.3@sha256:5f687152ab0661f84830877de93a8ec45cb0b756f71b24ac55133d03a4007936 + value: quay.io/konflux-ci/tekton-catalog/task-init:0.4.3@sha256:4be9343579d91c7b501cafe966cff59a601dfeca903c476e3763dd8d7599b900 - name: kind value: task resolver: bundles @@ -214,7 +214,7 @@ spec: - name: name value: git-clone-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-git-clone-oci-ta:0.2.6@sha256:3bcd4c39a346d29617013f4c47d245d38f020cdbf814cf3ce095911c528f5003 + value: quay.io/konflux-ci/tekton-catalog/task-git-clone-oci-ta:0.2.6@sha256:2e8fe30b6d5c8a8a3e6bbc0ea5a55e05b6170d4a399830f25ea43e17881ce544 - name: kind value: task resolver: bundles @@ -240,7 +240,7 @@ spec: - name: name value: prefetch-dependencies-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies-oci-ta:0.10.1@sha256:c09c1c3ced67fb8740b2925a7de09c0810d4e8c8f148d41c21ffe37810d85c62 + value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies-oci-ta:0.10.2@sha256:374f776bcb2048c3adeaf4dbb460c52d001bc6020320df5e878c2d05391302da - name: kind value: task resolver: bundles @@ -304,7 +304,7 @@ spec: - name: name value: buildah-remote-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-buildah-remote-oci-ta:0.12.0@sha256:44c1ca2e823f006ee94e066231b033b191f0f8cee51add1e908138335eba9b30 + value: quay.io/konflux-ci/tekton-catalog/task-buildah-remote-oci-ta:0.12.1@sha256:ceecb10bc58092c51a104f62ce6fd188a2d3f06fbdc446d81801cab118929344 - name: kind value: task resolver: bundles @@ -326,7 +326,7 @@ spec: - name: name value: build-image-index - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.3.1@sha256:c2cda69e681c976dd5b39caf6682d1c8f8c5e5a53a67a22a5b06d4cde773bd10 + value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.3.1@sha256:290c9ec319423ff9ae7b2cb78fa859e1d333abcdd2ef6c001533377812020071 - name: kind value: task resolver: bundles @@ -347,7 +347,7 @@ spec: - name: name value: source-build-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-source-build-oci-ta:0.3@sha256:6bb2697f8f194a6a644b8b861489ce8b1c5ba16a7d55dbd8e66a0c98d06dfc1c + value: quay.io/konflux-ci/tekton-catalog/task-source-build-oci-ta:0.3.1@sha256:1808485d95cf77fb7912f6fe69191bead05fc0f2f71e00031941a7ea38a5f665 - name: kind value: task resolver: bundles @@ -396,7 +396,7 @@ spec: - name: name value: roxctl-scan - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-roxctl-scan:0.1@sha256:76ed85aa05ce42d31341269837f104c3b766508313a68439085dcf9cc89d03f4 + value: quay.io/konflux-ci/tekton-catalog/task-roxctl-scan:0.1@sha256:97e2b2cdca9110fdc8a93ba585a1a1a743f989f2fd85f4073f6e5ea9ad2ce828 - name: kind value: task resolver: bundles @@ -421,7 +421,7 @@ spec: - name: name value: ecosystem-cert-preflight-checks - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:b961f8b259483fac39efc9371faca88faea528d2572ae30b79a9d3b56db8364b + value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:c07d2befa8abf48d4a223a5bdf5ea2335e756d4d0bbc422761087c263060aa94 - name: kind value: task resolver: bundles @@ -451,7 +451,7 @@ spec: - name: name value: sast-snyk-check-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check-oci-ta:0.5@sha256:97231d2d0fb3a54ef6f653e5a355afe965674f8386cf3c8339c6704751c146b5 + value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check-oci-ta:0.5@sha256:67a409de3c99aeaee4596da3081f26955ca6201a7f12cb6a9912659bdbcc4d01 - name: kind value: task resolver: bundles @@ -478,7 +478,7 @@ spec: - name: name value: clamav-scan - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.3.2@sha256:90be6a6537578b35687ae3639970be0eb273dfc73dabe4039bdcb307c555c086 + value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.3.3@sha256:9df272d2180bd67e6a3e04d5cd30ffa7ba27af4e599a1922f7069f5c89888e5b - name: kind value: task resolver: bundles @@ -574,7 +574,7 @@ spec: - name: name value: sast-shell-check-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-sast-shell-check-oci-ta:0.1@sha256:c06bee8d1655c153ef2b2234d44d824a5d613d54abf592b85702f81a73873bd0 + value: quay.io/konflux-ci/tekton-catalog/task-sast-shell-check-oci-ta:0.1@sha256:afa8ba8859739e48b672f66fa2af357d27f4d96846a7c0ad84e38f21b043f695 - name: kind value: task resolver: bundles @@ -602,7 +602,7 @@ spec: - name: name value: sast-unicode-check-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-sast-unicode-check-oci-ta:0.4@sha256:6a7fbfac7f7a9f95a2881e388d14e18e4fd9aacaa6d4ade9370fbed2c87dc989 + value: quay.io/konflux-ci/tekton-catalog/task-sast-unicode-check-oci-ta:0.4@sha256:69d5fca2fb94dcc7df32e36e4828e6fb24b9ba55b1837c331a83e20d8dfd479e - name: kind value: task resolver: bundles @@ -624,7 +624,7 @@ spec: - name: name value: apply-tags - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.3@sha256:2dae3c4beaf1070db565c536bc997e93925ae8c0c45dd8da9b4ae5293fd8c592 + value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.3.1@sha256:ccd3665345d86c6799bc7e2e6ad86b277d9f3a5c40b513e6f2a0af8ad92e7dba - name: kind value: task resolver: bundles @@ -647,7 +647,7 @@ spec: - name: name value: push-dockerfile-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile-oci-ta:0.3.1@sha256:393b4d0b530c2657b3ff6fec714781f4938b95b699dea2b369442b7664e1cce2 + value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile-oci-ta:0.3.1@sha256:ef00a86cb22259fcfdefa15a5116b63d0f24ee35c95d05ff9815ee8f84beb548 - name: kind value: task resolver: bundles @@ -664,7 +664,7 @@ spec: - name: name value: rpms-signature-scan - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2.2@sha256:f110c53fd58cab7ac6e47d472bb801d49e2f584eb31837db9d611e11b3862a3d + value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2.2@sha256:9ef4dabd53e823e3139b99c8de708be4ee759d63b32982847929df19ab75b2f8 - name: kind value: task resolver: bundles diff --git a/.tekton/lightspeed-stack-0-8-push.yaml b/.tekton/lightspeed-stack-0-8-push.yaml index 7e2388f7b..938ba86a1 100644 --- a/.tekton/lightspeed-stack-0-8-push.yaml +++ b/.tekton/lightspeed-stack-0-8-push.yaml @@ -194,7 +194,7 @@ spec: - name: name value: init - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-init:0.4.3@sha256:5f687152ab0661f84830877de93a8ec45cb0b756f71b24ac55133d03a4007936 + value: quay.io/konflux-ci/tekton-catalog/task-init:0.4.3@sha256:4be9343579d91c7b501cafe966cff59a601dfeca903c476e3763dd8d7599b900 - name: kind value: task resolver: bundles @@ -215,7 +215,7 @@ spec: - name: name value: git-clone-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-git-clone-oci-ta:0.2.6@sha256:3bcd4c39a346d29617013f4c47d245d38f020cdbf814cf3ce095911c528f5003 + value: quay.io/konflux-ci/tekton-catalog/task-git-clone-oci-ta:0.2.6@sha256:2e8fe30b6d5c8a8a3e6bbc0ea5a55e05b6170d4a399830f25ea43e17881ce544 - name: kind value: task resolver: bundles @@ -241,7 +241,7 @@ spec: - name: name value: prefetch-dependencies-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies-oci-ta:0.10.1@sha256:c09c1c3ced67fb8740b2925a7de09c0810d4e8c8f148d41c21ffe37810d85c62 + value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies-oci-ta:0.10.2@sha256:374f776bcb2048c3adeaf4dbb460c52d001bc6020320df5e878c2d05391302da - name: kind value: task resolver: bundles @@ -305,7 +305,7 @@ spec: - name: name value: buildah-remote-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-buildah-remote-oci-ta:0.12.0@sha256:44c1ca2e823f006ee94e066231b033b191f0f8cee51add1e908138335eba9b30 + value: quay.io/konflux-ci/tekton-catalog/task-buildah-remote-oci-ta:0.12.1@sha256:ceecb10bc58092c51a104f62ce6fd188a2d3f06fbdc446d81801cab118929344 - name: kind value: task resolver: bundles @@ -327,7 +327,7 @@ spec: - name: name value: build-image-index - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.3.1@sha256:c2cda69e681c976dd5b39caf6682d1c8f8c5e5a53a67a22a5b06d4cde773bd10 + value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.3.1@sha256:290c9ec319423ff9ae7b2cb78fa859e1d333abcdd2ef6c001533377812020071 - name: kind value: task resolver: bundles @@ -348,7 +348,7 @@ spec: - name: name value: source-build-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-source-build-oci-ta:0.3@sha256:6bb2697f8f194a6a644b8b861489ce8b1c5ba16a7d55dbd8e66a0c98d06dfc1c + value: quay.io/konflux-ci/tekton-catalog/task-source-build-oci-ta:0.3.1@sha256:1808485d95cf77fb7912f6fe69191bead05fc0f2f71e00031941a7ea38a5f665 - name: kind value: task resolver: bundles @@ -397,7 +397,7 @@ spec: - name: name value: roxctl-scan - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-roxctl-scan:0.1@sha256:76ed85aa05ce42d31341269837f104c3b766508313a68439085dcf9cc89d03f4 + value: quay.io/konflux-ci/tekton-catalog/task-roxctl-scan:0.1@sha256:97e2b2cdca9110fdc8a93ba585a1a1a743f989f2fd85f4073f6e5ea9ad2ce828 - name: kind value: task resolver: bundles @@ -422,7 +422,7 @@ spec: - name: name value: ecosystem-cert-preflight-checks - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:b961f8b259483fac39efc9371faca88faea528d2572ae30b79a9d3b56db8364b + value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:c07d2befa8abf48d4a223a5bdf5ea2335e756d4d0bbc422761087c263060aa94 - name: kind value: task resolver: bundles @@ -452,7 +452,7 @@ spec: - name: name value: sast-snyk-check-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check-oci-ta:0.5@sha256:97231d2d0fb3a54ef6f653e5a355afe965674f8386cf3c8339c6704751c146b5 + value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check-oci-ta:0.5@sha256:67a409de3c99aeaee4596da3081f26955ca6201a7f12cb6a9912659bdbcc4d01 - name: kind value: task resolver: bundles @@ -479,7 +479,7 @@ spec: - name: name value: clamav-scan - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.3.2@sha256:90be6a6537578b35687ae3639970be0eb273dfc73dabe4039bdcb307c555c086 + value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.3.3@sha256:9df272d2180bd67e6a3e04d5cd30ffa7ba27af4e599a1922f7069f5c89888e5b - name: kind value: task resolver: bundles @@ -575,7 +575,7 @@ spec: - name: name value: sast-shell-check-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-sast-shell-check-oci-ta:0.1@sha256:c06bee8d1655c153ef2b2234d44d824a5d613d54abf592b85702f81a73873bd0 + value: quay.io/konflux-ci/tekton-catalog/task-sast-shell-check-oci-ta:0.1@sha256:afa8ba8859739e48b672f66fa2af357d27f4d96846a7c0ad84e38f21b043f695 - name: kind value: task resolver: bundles @@ -603,7 +603,7 @@ spec: - name: name value: sast-unicode-check-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-sast-unicode-check-oci-ta:0.4@sha256:6a7fbfac7f7a9f95a2881e388d14e18e4fd9aacaa6d4ade9370fbed2c87dc989 + value: quay.io/konflux-ci/tekton-catalog/task-sast-unicode-check-oci-ta:0.4@sha256:69d5fca2fb94dcc7df32e36e4828e6fb24b9ba55b1837c331a83e20d8dfd479e - name: kind value: task resolver: bundles @@ -628,7 +628,7 @@ spec: - name: name value: apply-tags - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.3@sha256:2dae3c4beaf1070db565c536bc997e93925ae8c0c45dd8da9b4ae5293fd8c592 + value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.3.1@sha256:ccd3665345d86c6799bc7e2e6ad86b277d9f3a5c40b513e6f2a0af8ad92e7dba - name: kind value: task resolver: bundles @@ -651,7 +651,7 @@ spec: - name: name value: push-dockerfile-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile-oci-ta:0.3.1@sha256:393b4d0b530c2657b3ff6fec714781f4938b95b699dea2b369442b7664e1cce2 + value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile-oci-ta:0.3.1@sha256:ef00a86cb22259fcfdefa15a5116b63d0f24ee35c95d05ff9815ee8f84beb548 - name: kind value: task resolver: bundles @@ -668,7 +668,7 @@ spec: - name: name value: rpms-signature-scan - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2.2@sha256:f110c53fd58cab7ac6e47d472bb801d49e2f584eb31837db9d611e11b3862a3d + value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2.2@sha256:9ef4dabd53e823e3139b99c8de708be4ee759d63b32982847929df19ab75b2f8 - name: kind value: task resolver: bundles From 7ccf102bc96453b738506ea6f1ffd11f55c6e4ba Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Sun, 6 Sep 2026 09:51:49 +0200 Subject: [PATCH 012/120] Refactored if-else chain in mock MCP server --- tests/e2e/mock_mcp_server/server.py | 75 +++++++++++++++-------------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/tests/e2e/mock_mcp_server/server.py b/tests/e2e/mock_mcp_server/server.py index 3fa5a2df8..dbbdec8e7 100644 --- a/tests/e2e/mock_mcp_server/server.py +++ b/tests/e2e/mock_mcp_server/server.py @@ -73,44 +73,45 @@ def do_POST(self) -> None: # pylint: disable=invalid-name req_id = 1 method = "" - if method == "initialize": - self._json_response( - { - "jsonrpc": "2.0", - "id": req_id, - "result": { - "protocolVersion": "2024-11-05", - "capabilities": {"tools": {}}, - "serverInfo": {"name": "mock-mcp-e2e", "version": "1.0.0"}, - }, - } - ) - elif method == "tools/list": - self._json_response( - { - "jsonrpc": "2.0", - "id": req_id, - "result": { - "tools": [ - { - "name": "mock_tool_e2e", - "description": "Mock tool for E2E", - "inputSchema": { - "type": "object", - "properties": { - "message": { - "type": "string", - "description": "Test message", - } + match method: + case "initialize": + self._json_response( + { + "jsonrpc": "2.0", + "id": req_id, + "result": { + "protocolVersion": "2024-11-05", + "capabilities": {"tools": {}}, + "serverInfo": {"name": "mock-mcp-e2e", "version": "1.0.0"}, + }, + } + ) + case "tools/list": + self._json_response( + { + "jsonrpc": "2.0", + "id": req_id, + "result": { + "tools": [ + { + "name": "mock_tool_e2e", + "description": "Mock tool for E2E", + "inputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Test message", + } + }, }, - }, - } - ], - }, - } - ) - else: - self._json_response({"jsonrpc": "2.0", "id": req_id, "result": {}}) + } + ], + }, + } + ) + case _: + self._json_response({"jsonrpc": "2.0", "id": req_id, "result": {}}) def log_message(self, format: str, *args: Any) -> None: """Suppress request logging for minimal output.""" From e695256549f74c7062be71fe577453997aa9fc5a Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Sun, 6 Sep 2026 09:51:55 +0200 Subject: [PATCH 013/120] Refactored if-else chain in integration tests --- .../test_conversations_v1_integration.py | 43 ++++++++-------- .../test_conversations_v2_integration.py | 49 ++++++++++--------- 2 files changed, 48 insertions(+), 44 deletions(-) diff --git a/tests/integration/endpoints/test_conversations_v1_integration.py b/tests/integration/endpoints/test_conversations_v1_integration.py index bd8cca00e..131a29df1 100644 --- a/tests/integration/endpoints/test_conversations_v1_integration.py +++ b/tests/integration/endpoints/test_conversations_v1_integration.py @@ -236,26 +236,29 @@ async def test_conversation_validation_errors( # Call the appropriate endpoint with pytest.raises(HTTPException) as exc_info: - if endpoint == "get": - await get_conversation_endpoint_handler( - request=non_admin_test_request, - conversation_id=conversation_id, - auth=test_auth, - ) - elif endpoint == "delete": - await delete_conversation_endpoint_handler( - request=non_admin_test_request, - conversation_id=conversation_id, - auth=test_auth, - ) - elif endpoint == "update": - update_request = ConversationUpdateRequest(topic_summary="Updated summary") - await update_conversation_endpoint_handler( - request=non_admin_test_request, - conversation_id=conversation_id, - update_request=update_request, - auth=test_auth, - ) + match endpoint: + case "get": + await get_conversation_endpoint_handler( + request=non_admin_test_request, + conversation_id=conversation_id, + auth=test_auth, + ) + case "delete": + await delete_conversation_endpoint_handler( + request=non_admin_test_request, + conversation_id=conversation_id, + auth=test_auth, + ) + case "update": + update_request = ConversationUpdateRequest( + topic_summary="Updated summary" + ) + await update_conversation_endpoint_handler( + request=non_admin_test_request, + conversation_id=conversation_id, + update_request=update_request, + auth=test_auth, + ) # Verify error status code assert exc_info.value.status_code == expected_status diff --git a/tests/integration/endpoints/test_conversations_v2_integration.py b/tests/integration/endpoints/test_conversations_v2_integration.py index f81b0d139..f6fa0ef44 100644 --- a/tests/integration/endpoints/test_conversations_v2_integration.py +++ b/tests/integration/endpoints/test_conversations_v2_integration.py @@ -175,30 +175,31 @@ async def test_conversation_cache_unavailable_error_handling( test_config.conversation_cache_configuration.type = None with pytest.raises(HTTPException) as exc_info: - if endpoint == "list": - await get_conversations_list_endpoint_handler( - request=non_admin_test_request, - auth=test_auth, - ) - elif endpoint == "get": - await get_conversation_endpoint_handler( - request=non_admin_test_request, - conversation_id=conversation_id, - auth=test_auth, - ) - elif endpoint == "delete": - await delete_conversation_endpoint_handler( - request=non_admin_test_request, - conversation_id=conversation_id, - auth=test_auth, - ) - elif endpoint == "update": - update_request = ConversationUpdateRequest(topic_summary="New topic") - await update_conversation_endpoint_handler( - conversation_id=conversation_id, - update_request=update_request, - auth=test_auth, - ) + match endpoint: + case "list": + await get_conversations_list_endpoint_handler( + request=non_admin_test_request, + auth=test_auth, + ) + case "get": + await get_conversation_endpoint_handler( + request=non_admin_test_request, + conversation_id=conversation_id, + auth=test_auth, + ) + case "delete": + await delete_conversation_endpoint_handler( + request=non_admin_test_request, + conversation_id=conversation_id, + auth=test_auth, + ) + case "update": + update_request = ConversationUpdateRequest(topic_summary="New topic") + await update_conversation_endpoint_handler( + conversation_id=conversation_id, + update_request=update_request, + auth=test_auth, + ) # Verify error details (all should return 500) assert exc_info.value.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR From b2ba361c50ede0f8e5fb2b49bd19c082dc624c6b Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Sun, 6 Sep 2026 09:52:07 +0200 Subject: [PATCH 014/120] Refactored if-else chain in unit tests --- tests/unit/authorization/test_middleware.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/unit/authorization/test_middleware.py b/tests/unit/authorization/test_middleware.py index adffe56d1..e990a96c1 100644 --- a/tests/unit/authorization/test_middleware.py +++ b/tests/unit/authorization/test_middleware.py @@ -315,12 +315,13 @@ async def test_request_state_handling( kwargs = {"auth": dummy_auth_tuple} args = [] - if request_location == "kwargs": - kwargs["request"] = mock_request - elif request_location == "args": - args = [ - mock_request, - ] + match request_location: + case "kwargs": + kwargs["request"] = mock_request + case "args": + args = [ + mock_request, + ] await _perform_authorization_check(Action.QUERY, args, kwargs) From bd68ef9292c2a338dfe975a9c687fa2f56590301 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Sun, 6 Sep 2026 09:54:13 +0200 Subject: [PATCH 015/120] LCORE-3298: cleanup --- scripts/vulnerability_report.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/vulnerability_report.py b/scripts/vulnerability_report.py index 440506004..b5b539e51 100644 --- a/scripts/vulnerability_report.py +++ b/scripts/vulnerability_report.py @@ -457,13 +457,13 @@ def generate_overal_state_graph( svg_output (bool): Whether to save in SVG format. png_output (bool): Whether to save in PNG format. """ - fig, ax = plt.subplots() + _, ax = plt.subplots() data = stat["state"] ax.bar( range(len(data)), list(data.values()), align="center", color=["#c00000", "#00c000"] ) ax.set_ylim(top=400) - ax.set_xticks(range(len(D)), list(D.keys())) + ax.set_xticks(range(len(data)), list(data.keys())) save_graph(prefix, "state", svg_output, png_output) From fd3d288ed482ea307a2627d69891f9973b96a9a6 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Sun, 6 Sep 2026 10:00:06 +0200 Subject: [PATCH 016/120] LCORE-3580: Updated dependencies --- uv.lock | 151 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 76 insertions(+), 75 deletions(-) diff --git a/uv.lock b/uv.lock index 4df43a434..6e69f2411 100644 --- a/uv.lock +++ b/uv.lock @@ -169,7 +169,7 @@ wheels = [ [[package]] name = "anthropic" -version = "1.3.0" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -180,22 +180,22 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/50/463166f02179ab279edb61de1589a6f69cb3838d6a2fb6f2c92a3f8042f1/anthropic-1.3.0.tar.gz", hash = "sha256:6873492a77ede8849a161ab1bc78bc9a1e492a006d0b5bb4c57ac77845df838a", size = 1148177, upload-time = "2026-09-01T17:37:10.392Z" } +sdist = { url = "https://files.pythonhosted.org/packages/75/6d/793f5cfe2cd444c43b4eeb4cb7c3cc55ebcb38929fdfe81aa1f2fced7326/anthropic-1.4.0.tar.gz", hash = "sha256:f0d017e901e48b343520b5d458f8240c283c8d850bf6d119834c622207e0a74c", size = 1150831, upload-time = "2026-09-04T22:20:31.355Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/5d/7863a9961d320c23787c7b594956afe4e878f9c0ae2376b11a20e416791d/anthropic-1.3.0-py3-none-any.whl", hash = "sha256:e7e7dbebf9f3c84a23954ab989378af6ae10a4d1804c81e9fea4b5ced695ce75", size = 1296959, upload-time = "2026-09-01T17:37:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e3/34a88f0e1e854022a352d67f72ca9baa8b952d4541315088411ba2bfbc2a/anthropic-1.4.0-py3-none-any.whl", hash = "sha256:590e85bff75b713a123b03f586d68f02266b5fdc49f70dd75f721ced93a4716c", size = 1300390, upload-time = "2026-09-04T22:20:33.078Z" }, ] [[package]] name = "anyio" -version = "4.15.0" +version = "4.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/9a/c15a60547004a3f3cea20296c934f827ddd7bdba225a2e7e9fcb5ec48c80/anyio-4.15.0.tar.gz", hash = "sha256:b5c620ed540725e2579c31b17bb995b3bf02c9281c9cace04c7d186380bab85e", size = 276504, upload-time = "2026-09-02T21:46:36.957Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/21/a6/2b21ce5ebe4d8938a247c9b0dbb7271566ae559b01795c83ea4bb2660ed7/anyio-4.15.0-py3-none-any.whl", hash = "sha256:7ecd9937369ffce8bba0b5ccb9b3a9507b101b0ed50256aecfbab27e6c2acb99", size = 131908, upload-time = "2026-09-02T21:46:35.485Z" }, + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, ] [[package]] @@ -429,30 +429,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.43.87" +version = "1.43.89" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/0c/b14374e9458030076cd22ff9381cf86d170f31b648fd901db1d88011094b/boto3-1.43.87.tar.gz", hash = "sha256:8d9521c7c292194b8ce9fb61043d52e45cdba29b5f690981f3eb5e75103ba57d", size = 112691, upload-time = "2026-09-02T19:23:13.603Z" } +sdist = { url = "https://files.pythonhosted.org/packages/50/26/48b3da85526a72a02df55e564481fc348e93699c15f0f502681b12ac2c8a/boto3-1.43.89.tar.gz", hash = "sha256:c28abbe472e9b7cad08807356311aeec51bde5218c18489da827045d2267bfd9", size = 112702, upload-time = "2026-09-04T19:24:57.143Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/a9/7dd7cc1a2387f3c78f66e49682957b7e7d590706dc6552d108b48125022d/boto3-1.43.87-py3-none-any.whl", hash = "sha256:671cf88a353f887774603980898bc34249d05a994e1c5e24aba4f4e1bbef0951", size = 140026, upload-time = "2026-09-02T19:23:12.044Z" }, + { url = "https://files.pythonhosted.org/packages/cd/12/e1b5cb4a00a9bfd72cf2d3f982c5826757aacdfc90aa4bd61902dcc94856/boto3-1.43.89-py3-none-any.whl", hash = "sha256:fe4190afe63eb562b6ba6a3911cf4427473b35fa047adde093bf696d3ae09fc0", size = 140028, upload-time = "2026-09-04T19:24:55.929Z" }, ] [[package]] name = "botocore" -version = "1.43.87" +version = "1.43.89" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/c4/64ebb159810a9840c57659f3ed98439bbc5680dc9708e3b08212deea301a/botocore-1.43.87.tar.gz", hash = "sha256:928598e7275fa70385d7f694d60d59afe115a0b914cc699dbf3eb60954c23bf1", size = 16064894, upload-time = "2026-09-02T19:23:08.777Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/06/f63fb1befdf77af18539fb24ea01f2da0f13965ed5de091061708ac96416/botocore-1.43.89.tar.gz", hash = "sha256:f0574942970742657b0e0716cf08c2dfe6bef8e6de5fbb7081c3424e262b4cca", size = 16074206, upload-time = "2026-09-04T19:24:52.464Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/5c/f15aa7a5da42fc85254f0150df7d377bb965b0c130c68f9701fba88e0df4/botocore-1.43.87-py3-none-any.whl", hash = "sha256:01e1e2255a8f4b959c19b210d74a5a916d13841c0344ceee15d8a3eaa4daa892", size = 15755750, upload-time = "2026-09-02T19:23:05.937Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9d/96f9dee6d12eedf1c2b4264eefd59c7ac8cac10daadb9a7bfccc9ee881c6/botocore-1.43.89-py3-none-any.whl", hash = "sha256:d7211220c815427fe71225acc6909e4ab5dfab3b03770e72fd16cf9eb86b3d1a", size = 15768272, upload-time = "2026-09-04T19:24:49.769Z" }, ] [[package]] @@ -1097,18 +1097,19 @@ wheels = [ [[package]] name = "google-api-core" -version = "2.34.0" +version = "2.36.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth" }, { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, { name = "proto-plus" }, { name = "protobuf" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/7c/9be3903e3d45415e8ca493c75f8990a0f6f579d168015d44c379350d0ab0/google_api_core-2.34.0.tar.gz", hash = "sha256:98a779fe72de956eb1c9c2f47ff4c4432a668ece1a002ec38bed07ec2698ae59", size = 187953, upload-time = "2026-08-06T06:23:58.128Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/d8/88c2f0e6b0dd46a7796cca64fad99c7adba2417916f5393e82b9b7d2548e/google_api_core-2.36.0.tar.gz", hash = "sha256:32779307b52e64c9a9592a3621de6281676ecaeea299fe8524e4637ab7ac2531", size = 196879, upload-time = "2026-09-03T22:30:51.216Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/c1/a8a92ae1bc4b1a8f804c776d7d3f0c771b78a62c3ad4df1be41b3fd8c767/google_api_core-2.34.0-py3-none-any.whl", hash = "sha256:cdf9c67e7ca2402d86ccbfde5f2503fc83e3cc3f58cc78456ae96cad24a6d2de", size = 180545, upload-time = "2026-08-06T06:22:47.502Z" }, + { url = "https://files.pythonhosted.org/packages/44/56/30c91c61b8f70d4c09285a005b94798729aeaf4ec8b90c1c360da8207728/google_api_core-2.36.0-py3-none-any.whl", hash = "sha256:e4d0b179260727ea5c42222426d9199285214dbef7bf48f8b16600c7f9a78944", size = 184118, upload-time = "2026-09-03T22:30:06.662Z" }, ] [package.optional-dependencies] @@ -1119,15 +1120,15 @@ grpc = [ [[package]] name = "google-auth" -version = "2.57.0" +version = "2.57.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/64/55f316b729f92a552d26e00aa3b1542b2e149d0a5efe2842afff0cac7af7/google_auth-2.57.0.tar.gz", hash = "sha256:9b4f96d6a1feb5f7201231f47cfb3de08d8f176f8a61f9e461555116e95a8789", size = 370794, upload-time = "2026-08-25T19:18:26.419Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/3a/d3982b28d267880b5641f9a32c55d8062a9d2bad2d28d0274285391c89c2/google_auth-2.57.1.tar.gz", hash = "sha256:eb47b230fc6707eed4aee1c9cef55ec05bc1785eecba74ff8b572d531e921b1e", size = 372426, upload-time = "2026-09-04T00:50:27.593Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/f3/8508a702c094af5f6e89773f4dfdeee74913df0f41a02c21b5e7dc3d75cd/google_auth-2.57.0-py3-none-any.whl", hash = "sha256:180dafe015cfb62193bea26b677500fab5b9fd51a1e825ebf3ad9b182047ae59", size = 259728, upload-time = "2026-08-24T21:55:08.449Z" }, + { url = "https://files.pythonhosted.org/packages/3b/27/0f7247b8002a1404fdb5412aeb15fb0fe70288eb8f88a5873d1c0d8262cf/google_auth-2.57.1-py3-none-any.whl", hash = "sha256:ab439dee60a6856412bc058f13a68eb6a59c5b81526bb89cfdcf89ce9c0a48c9", size = 259974, upload-time = "2026-09-04T00:50:21.693Z" }, ] [package.optional-dependencies] @@ -1164,7 +1165,7 @@ wheels = [ [[package]] name = "google-cloud-bigquery" -version = "3.44.0" +version = "3.45.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, @@ -1175,9 +1176,9 @@ dependencies = [ { name = "python-dateutil" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/fb/86d5bfdfbd9d810f6a49eb3e9cc591a2b46d33262338acc244cc79d032c1/google_cloud_bigquery-3.44.0.tar.gz", hash = "sha256:30651ae469b419f450b9c96581fd4942e2e060490df1ac0314bf379f16883215", size = 527575, upload-time = "2026-08-25T19:18:36.317Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/a5/64ba829eb6c8acf5c0a80b4c9b217c286a2cd592f661121660de9cdba329/google_cloud_bigquery-3.45.0.tar.gz", hash = "sha256:5a799856825f47743ce561802cbd01b4d5c70062b36a942cfef5146017ecf0a4", size = 528642, upload-time = "2026-09-03T22:30:57.289Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/84/af193ca97ce72b56fd05f8ad15776bf7446d99cb0ba1cd2f4880dbc52543/google_cloud_bigquery-3.44.0-py3-none-any.whl", hash = "sha256:ac2f0a6ab61a3c742ba4674dc220fb98461c13e1def96fe7a980ef7d5e0c0285", size = 267691, upload-time = "2026-08-24T21:55:19.249Z" }, + { url = "https://files.pythonhosted.org/packages/ce/af/f1c877f3d47a616f19003ba958cec5efe6e8d43d3576c5827bf53ae94ad2/google_cloud_bigquery-3.45.0-py3-none-any.whl", hash = "sha256:30637314d67526cbaefebfb9106c5a41c5f30a137ace404531148f09a6b487a6", size = 267964, upload-time = "2026-09-03T22:30:15.283Z" }, ] [[package]] @@ -1280,14 +1281,14 @@ wheels = [ [[package]] name = "googleapis-common-protos" -version = "1.75.2" +version = "1.75.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/90/fb8f1c84537fbf210c1f53a53ae473a805f6599c5a40b93c1bbadd211f7a/googleapis_common_protos-1.75.2.tar.gz", hash = "sha256:8829a3d1e4508c5b7b9a6b9525f7fccff611f8531644579a76466c29295d4bb2", size = 154083, upload-time = "2026-08-25T19:19:13.028Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/c5/4353a188e2c335aee33269e8b654af228278cca8e5f0b4b5f11e5d0e9adb/googleapis_common_protos-1.75.3.tar.gz", hash = "sha256:57c435ac2c68b108999b6db075d9053e4d7a936ba57b4a3d45667b1346f1738a", size = 153905, upload-time = "2026-09-03T22:31:21.869Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/5b/1c9e55363c3b1890a98cae813de5b4ea327845756cd8fb7ee690140c7eac/googleapis_common_protos-1.75.2-py3-none-any.whl", hash = "sha256:6b83302f554ea93a0f48409c7fc2050f954bcbcddb7e3a9c76d4a823cb22920e", size = 307002, upload-time = "2026-08-25T19:18:08.927Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7a/7d79170c6ce6f12e109df2b3879d6b934010cf4f99aea8de8b7e5408c174/googleapis_common_protos-1.75.3-py3-none-any.whl", hash = "sha256:a018d2bf098ca9fb6faa08d5bb780e2a2c2f73c566f069761331386c9596d3f2", size = 306984, upload-time = "2026-09-03T22:30:45.133Z" }, ] [package.optional-dependencies] @@ -1325,11 +1326,11 @@ wheels = [ [[package]] name = "griffelib" -version = "2.2.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/b4/a767e91c606deefc447a96eaf59edd77397960b1d677dffd833ee8449831/griffelib-2.2.0.tar.gz", hash = "sha256:e1bc36fe9cd21d4b6b659b456346755e4cfdc5676c0a5214083126ee12612b3c", size = 227048, upload-time = "2026-08-16T14:04:58.383Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/af/018c10bc9edd42b6ef6db2e96b09542050d5253f9b195e74bc910b2d13ab/griffelib-2.3.0.tar.gz", hash = "sha256:7b0952caf5bca6afa4bb5ee8c6a2d183fe3f21b62efc5f6c7243cb2b26d2d115", size = 234534, upload-time = "2026-09-04T15:08:17.472Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/b6/f65ac785d4ac90dcf7c831ac6256f5dd4a19780f4e1575b2c0d6eeebe319/griffelib-2.2.0-py3-none-any.whl", hash = "sha256:d71c3bc2bbed9f958488634fe788b843a9f705d6d2838ca32cd6c25eeb64dfc4", size = 166779, upload-time = "2026-08-16T14:04:54.365Z" }, + { url = "https://files.pythonhosted.org/packages/41/63/e876e789525063c840ccfa8857febdabd6523bcef9ce7eb979b9305ea895/griffelib-2.3.0-py3-none-any.whl", hash = "sha256:1b8f9cd525681c26b1d6d574faa1371651e8459ca51d209684f50b8096ae06e0", size = 169423, upload-time = "2026-09-04T15:08:12.956Z" }, ] [[package]] @@ -2064,7 +2065,7 @@ wheels = [ [[package]] name = "litellm" -version = "1.99.0" +version = "1.100.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -2082,20 +2083,20 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6c/1a/76b5f28ba9e07fa0cb807bbdefdf318c554f60623bff699e7b601d0d7b3a/litellm-1.99.0.tar.gz", hash = "sha256:594bf4b6ff6b79c6aa3c3b78c0e939d4afd12687e076ba3fb608d38a5aa7f9c6", size = 16786386, upload-time = "2026-09-01T00:43:07.434Z" } +sdist = { url = "https://files.pythonhosted.org/packages/03/ce/1e1ce2558f65244057c0c40c60ade4dfa3767da3522bf1dd8679507ad7ba/litellm-1.100.0.tar.gz", hash = "sha256:ece94e817a453a5b3a9517c03547c428d501cea719edb728c1b260e53f78ea35", size = 17287857, upload-time = "2026-09-06T00:22:46.023Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/ef/4ef1afe95d3a7c52d63b5f2bb32ec5264a4cea198a7dddb8ceff9633f2da/litellm-1.99.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:a43e8716da8beed04480e91b4233ff2f1ab1fedd84cad332dbc526b54a9229ca", size = 23350560, upload-time = "2026-09-01T00:42:41.449Z" }, - { url = "https://files.pythonhosted.org/packages/03/8d/f329cf74fc1f929a93210b16523bbcebd679694090df741bb8bee726a473/litellm-1.99.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e2b383070656fdbec4bc44602edaaed2a21e99ceee4ea0a4650c8cb381e67b59", size = 22999186, upload-time = "2026-09-01T00:42:44.978Z" }, - { url = "https://files.pythonhosted.org/packages/bf/4f/0e73fc3f0740249b676d0714bd58c4141f1fc733b88a85b6001f9f2e5e62/litellm-1.99.0-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e461b7ce53af990e5287cf7ae30d82c956b56dcce886f63ef39a76e678f82a3b", size = 23145753, upload-time = "2026-09-01T00:42:48.19Z" }, - { url = "https://files.pythonhosted.org/packages/db/c4/8e92a6277e28096784616cfda16b2cd839c7bdaabce692ae950e2f0cf72e/litellm-1.99.0-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1c45097e426fed2ae7fbd38b5404c3addeb203d0e1148c0a59848aabd5fe83c6", size = 23518654, upload-time = "2026-09-01T00:42:51.582Z" }, - { url = "https://files.pythonhosted.org/packages/a9/aa/11c9bd6297e4a9001426ff825bc8bb119d3a9a3de4e5ee9fed22997e6e28/litellm-1.99.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e42f94731665b68e263481efd79f7629e9b97eed7e10c57dc37a890eca058227", size = 23218889, upload-time = "2026-09-01T00:42:54.99Z" }, - { url = "https://files.pythonhosted.org/packages/3f/a9/1fc21c6fb21897867d9c753d651b24903e63955dadffeb2a73517f35003a/litellm-1.99.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:71109c323164b4b6776ff259876523e6e883a465aa1413dd51a1bda8e92efc5f", size = 23617457, upload-time = "2026-09-01T00:42:59.173Z" }, - { url = "https://files.pythonhosted.org/packages/7c/4f/df449e0cfa1f40025e91f3a47b03109e16de44d43149e97fbcbd1ae8c5e2/litellm-1.99.0-cp310-abi3-win_amd64.whl", hash = "sha256:5617804e838499bce8fecb41ad9bc984b7977361e557666fed0fef0c4623ce62", size = 23432066, upload-time = "2026-09-01T00:43:03.177Z" }, + { url = "https://files.pythonhosted.org/packages/87/92/983d15efffd9bab37ebbff255fb09417814ecd959bd2a2459b8afb6c205d/litellm-1.100.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:098a413e398e2220734cf9c8dd75fb34a38b65b64090619c2869af1fdeaf4ae5", size = 23899430, upload-time = "2026-09-06T00:22:21.941Z" }, + { url = "https://files.pythonhosted.org/packages/59/e1/ea3f868ad6c84b2232f41d5d56adb5f0794ebb1cf32fc0e78effa5842170/litellm-1.100.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:0f87fae695edbca27e5cf970bea52fa405fa9910fdf7c29c963d6413db767299", size = 23552365, upload-time = "2026-09-06T00:22:25.456Z" }, + { url = "https://files.pythonhosted.org/packages/61/50/9439bd3238c9d8c7efcd5165555f175cc8e99e748ba99e8cc7e502787265/litellm-1.100.0-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a07370d116905485e9ac99679bac991a0a6c81e13c99ca3f007128fbdf2b0082", size = 23693340, upload-time = "2026-09-06T00:22:28.549Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/34a1df3a5fe1db92bf43d3b25af2e5a9c36c54ab6813acef0c31cce2b4e0/litellm-1.100.0-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:8224c8eed9cab3319a88e6665d1275ad8faf21d353b1b22223a6d6115a302ea2", size = 24063361, upload-time = "2026-09-06T00:22:31.913Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d0/4c7e8c8402e5af8c9f55602484c2bc888f0a8fe5c2a01df2a3705ba2139b/litellm-1.100.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e3787fb7ad1f20aebdde7686a85061880bba6550c9d62bfa1cf8df089fe7899b", size = 23766707, upload-time = "2026-09-06T00:22:35.06Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a2/81595fcda1457b777739d265b9383c1061c884af37111223b5fecbf72588/litellm-1.100.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:0b7ec93013e18535481cd811b776ee95c6b957b3a9fb44dc53f7c459e7e60e38", size = 24163673, upload-time = "2026-09-06T00:22:38.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/96/1ee7a3c86c4e9afec528d094b30158cd55b8bbc619e1632ad897bbd1291a/litellm-1.100.0-cp310-abi3-win_amd64.whl", hash = "sha256:c6f2f56808d05d8d2a7766129d958101eafb1a47e30ba5ddcfa900cdbc50af67", size = 23974360, upload-time = "2026-09-06T00:22:42.783Z" }, ] [[package]] name = "logfire" -version = "4.41.0" +version = "5.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "executing" }, @@ -2106,9 +2107,9 @@ dependencies = [ { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/64/1a/529f5fd3d0b72eca62737e07b290d38737f104f31891d23e5ed47a8ec7a0/logfire-4.41.0.tar.gz", hash = "sha256:3806fba60389d57c38a12a88135a7c7bf9d0fca09325094517e976b29b5b9d33", size = 1302531, upload-time = "2026-08-20T17:42:23.037Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/92/a45577fe3291aa85685330bb349108d7ce679d4dc094b8f8722d46b7fe11/logfire-5.0.0.tar.gz", hash = "sha256:579246cb37d767d88d0c33a14735b10b1c58347fd6a8def5a757f9ce740768db", size = 1346561, upload-time = "2026-09-04T18:44:52.329Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/e1/ee33bf0e3f85c00a4235c8c9c4e23f3955154d842f15f0377936461ec6a3/logfire-4.41.0-py3-none-any.whl", hash = "sha256:5bae36637aef81eeee6bfa5d764bf3cff0755af613a4888fd0ae4a656cd2451e", size = 426654, upload-time = "2026-08-20T17:42:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/da/a0/f3a6a31587b85283cbb878446b2e55c9152624586d8d7322adab124fbc96/logfire-5.0.0-py3-none-any.whl", hash = "sha256:6b0179c641b210eefa971f9bf082faed5f722d003f6c7e744c6362dabb8dbe18", size = 467846, upload-time = "2026-09-04T18:44:48.63Z" }, ] [package.optional-dependencies] @@ -2118,11 +2119,11 @@ httpx = [ [[package]] name = "logfire-api" -version = "4.41.0" +version = "5.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/83/a2e7de43bb092ffaad904b5756cfc1e0ea4a8d79fdacd24cd55e60790585/logfire_api-4.41.0.tar.gz", hash = "sha256:ec39252acac38b5b50d60cfb9cc62f0ea10c841345fc59692af32dbe3de4a140", size = 92818, upload-time = "2026-08-20T17:42:24.302Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/bb/3ee615e089eae6b11c61bc3a34d58256942210f81aa4884962ef1b9bde01/logfire_api-5.0.0.tar.gz", hash = "sha256:c018a16cd36a8ec20c6c6c316d3822788573ccb573e1b29a8be7a78d778e7775", size = 95611, upload-time = "2026-09-04T18:44:53.935Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/96/97552d0d742866719b3a6e8fd7e68dd2877804f4c3b10c44a19a1f99b0d6/logfire_api-4.41.0-py3-none-any.whl", hash = "sha256:c71010d086c0211b04b4640181e836e8a75cc635fcfa7f712557f7b0676c1413", size = 143003, upload-time = "2026-08-20T17:42:21.764Z" }, + { url = "https://files.pythonhosted.org/packages/36/67/282646935c5af564ca89447f745aa292e2d36b554fac3d1effe5c75495ca/logfire_api-5.0.0-py3-none-any.whl", hash = "sha256:a95cc00c679ddcb98fb53c425bdebb6008a9498fb989e89f280283cb00a58a74", size = 145861, upload-time = "2026-09-04T18:44:50.673Z" }, ] [[package]] @@ -3540,11 +3541,11 @@ wheels = [ [[package]] name = "pypdf" -version = "6.16.2" +version = "6.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/66/54212e75406afd9f3e933d0dda23072f6aecc55c5a273077dc2e0b028b23/pypdf-6.16.2.tar.gz", hash = "sha256:595647f6191de6f402cfde1d0c455d6cbccbd509aac32b34783009c032de5d6e", size = 7008996, upload-time = "2026-08-23T13:50:07.135Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/dc/34857a5e31cf708c163929f61a9ba4bd357a8850e49fc4e846ced527b51f/pypdf-6.17.0.tar.gz", hash = "sha256:097ad0d829778ec5b615aeaa5c6da4b6cac4992f8fd80b56f98a1a8c006573bb", size = 7018352, upload-time = "2026-09-04T11:30:44.256Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/f1/a2da3b55acd4ab737bf728c97edaaed5ec1d3c1236acb639dcdfa97e42c7/pypdf-6.16.2-py3-none-any.whl", hash = "sha256:c8b09a59399062fb45a1b8156c18a787a10a3dae03ac9674397a226712c94604", size = 385060, upload-time = "2026-08-23T13:50:05.349Z" }, + { url = "https://files.pythonhosted.org/packages/c1/08/1e9731038124a9127e1d27848952b86fb32b2f45f8f1b94adc7f0817a6ac/pypdf-6.17.0-py3-none-any.whl", hash = "sha256:5bd827266a21553b74d910e350131a6227b72f2ab4209bf372814b8195fa11c5", size = 388051, upload-time = "2026-09-04T11:30:42.681Z" }, ] [[package]] @@ -3972,27 +3973,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/85/c8e12473c93018f92d19dd988a294202e1c27426c47ec4de53ffb847b8d8/ruff-0.16.5.tar.gz", hash = "sha256:1b88500f9ffbcab3dedb0082c9f9492e91ec3d618aac1236a3e0189938f7040b", size = 4912003, upload-time = "2026-08-27T16:34:18.258Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/b6/77c90a970fe2dae17a723acbd011043ea97c98d7deacccefdc4ba74ec512/ruff-0.16.5-py3-none-linux_armv6l.whl", hash = "sha256:12e5f673e774c35fbb62f288809c7653b73445f8ecec6b6063fd6ea3521aa14b", size = 10011941, upload-time = "2026-08-27T16:33:41.287Z" }, - { url = "https://files.pythonhosted.org/packages/4b/46/6cf67cf6411885a1d6f7f6d801682f155536a85176d10b605e2ceffed8bd/ruff-0.16.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:eda58a5802de40e7ed5b32b64e0b32539338cc6fcd2c78f61e3ad6a0d79f51c3", size = 10204049, upload-time = "2026-08-27T16:33:44.056Z" }, - { url = "https://files.pythonhosted.org/packages/46/fd/c8720ca7a090abf0c2fef4abe8a5ef6e5127ed15196d8886ff75a2b370e2/ruff-0.16.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5ae9a7b9a8875131f40f8fe967cc86abf899779efd663cb7ce3d572d01da7eb", size = 9809037, upload-time = "2026-08-27T16:33:46.257Z" }, - { url = "https://files.pythonhosted.org/packages/43/45/a684caacdedaca180f52bacccc40bf0789d2c5a7c75f25324853e9eaedb5/ruff-0.16.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b719b0a1f4d59710d283ab2965f621684a108a9e41da622e3b23f0326cd0025", size = 9964129, upload-time = "2026-08-27T16:33:48.352Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/5d2bcdaca6b5b93d1b4dfc166cd2aebf7680143a1b38a28759df13a94d31/ruff-0.16.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2298f2780ed1be0c5cb1361e32ab7b1467f3cce7dabe101d2210a314f2fe42e9", size = 9821518, upload-time = "2026-08-27T16:33:50.57Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ff/011cce29accf9257d5974145b733fc653a37985ed6825413a3987cefbfe0/ruff-0.16.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:258f29035a2dd021e7861e631b227a5b3f14e50c1184c9a6a122c5f4576154d7", size = 10534835, upload-time = "2026-08-27T16:33:52.522Z" }, - { url = "https://files.pythonhosted.org/packages/d7/5a/f0cf109bada9bba0e96c90c21c9f9251803f57225c32d293327a03c710d6/ruff-0.16.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9a4f0432966834019c74d1b7e5c51224305d7713f3d7faf3e7451f1a3be3cde", size = 11252550, upload-time = "2026-08-27T16:33:54.521Z" }, - { url = "https://files.pythonhosted.org/packages/63/4d/1d481aaea2046c6a7ed7c291f9004c669cce3c087b6b376ed5b08271e3fe/ruff-0.16.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5eb3a8c3d0ade9cea42b591fd530368e8798380e30e0a308b85a5cf718f09ea", size = 10777949, upload-time = "2026-08-27T16:33:56.88Z" }, - { url = "https://files.pythonhosted.org/packages/ee/34/ee245ca55f64443233034b3d02b03236b19242004281247c079390b7facd/ruff-0.16.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef0f69e191a13a3c9816f63163c88790cb12cd157bbbb384e9c44745702ab105", size = 10311656, upload-time = "2026-08-27T16:33:59.12Z" }, - { url = "https://files.pythonhosted.org/packages/a7/4d/c33a333e341c0a2b96c715b52d89a606f5a34cd4ac493cd9b8d0187186b8/ruff-0.16.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0eeab41fbea2c42f98dfb9822cdccda9d24ba38d49f6dc945b5c236d48f0ef29", size = 10532125, upload-time = "2026-08-27T16:34:01.166Z" }, - { url = "https://files.pythonhosted.org/packages/30/e1/a64cef78b40192497bb98a27a8aa8f2c98ee9ee15bc97f7712d94ef32937/ruff-0.16.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f0768e9df4300713fff30733c87575f68b6f1d8de41184e505b7fdd9c0c95eaf", size = 10097648, upload-time = "2026-08-27T16:34:03.16Z" }, - { url = "https://files.pythonhosted.org/packages/cc/4e/4cdc9ed3c3e109d2f71e62572a37457298d7bc7501ec3138babb7ed32bbd/ruff-0.16.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:95cc70cdc7aa80c338de356279d2adbeb2de0f520b9ecd8aba75b94e95e02f91", size = 9829344, upload-time = "2026-08-27T16:34:05.134Z" }, - { url = "https://files.pythonhosted.org/packages/39/4a/31ed35ce31729955fc583ee0d176d6e784c1290cb0b0a75cb2134c1ab72a/ruff-0.16.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d185c8398ded1bfd91c0c2cb258346307571eccc473a8490af8c3977399c384a", size = 10277117, upload-time = "2026-08-27T16:34:07.425Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a0/60356d86687b4b666d593df213f4dc3041750d024cb7bf2cfa81cfd65c2e/ruff-0.16.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fb8e3a3c4c6a784150a7ced53b015f4b253fc2bf97a610886419ead64b4756ef", size = 10711653, upload-time = "2026-08-27T16:34:09.712Z" }, - { url = "https://files.pythonhosted.org/packages/ed/20/656d67f5b25ca9bda4e02b1de25867b2954e1d19e03648060f167ad0f4cc/ruff-0.16.5-py3-none-win32.whl", hash = "sha256:288b0a5f080492fe5635db849f9e2e84aa3cce7b7f0e955997d416c507c76a26", size = 10034250, upload-time = "2026-08-27T16:34:11.8Z" }, - { url = "https://files.pythonhosted.org/packages/5b/42/ee8e68a207b9127fcde6c3d7e197def432f346cb1af159e1fa14ca0d1cdc/ruff-0.16.5-py3-none-win_amd64.whl", hash = "sha256:ddc6385fb2137f616357ca03d6c74f4be987f80fed4008566b754f6032b8546f", size = 10516714, upload-time = "2026-08-27T16:34:13.963Z" }, - { url = "https://files.pythonhosted.org/packages/73/e3/7df5a396e445b9ba49ce9a9437439a4d80042c61c0ade199abf8d16de1ac/ruff-0.16.5-py3-none-win_arm64.whl", hash = "sha256:a64abe90968719b851bb7cedffaa8753fbdbdadab483089682db623f3edc587e", size = 10391564, upload-time = "2026-08-27T16:34:16.064Z" }, +version = "0.16.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/7c/6adb35d70e7c027e308274557901c7e00fb3407750faf3620c184ae058cb/ruff-0.16.6.tar.gz", hash = "sha256:dcf8a73d2ff77e99dde91244b4da16feba7f14e6beeb4015dee7c5a909e99050", size = 4921251, upload-time = "2026-09-03T16:57:29.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/28/9cc1b79639e284ec103f43c88c644db4eb58cbd0ea1ca11f1193435369ac/ruff-0.16.6-py3-none-linux_armv6l.whl", hash = "sha256:61c368c26bf8e973e5ab14a2772de587bc068ea3f9a277f673380749b4898fb8", size = 10015638, upload-time = "2026-09-03T16:56:40.986Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/627d342ef727ea7794edf74fe23d60a074b02c3acc2e9436684e782286ca/ruff-0.16.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ecf4f068e2e123e43a26e9db4e19524cc56563912404e83bbfca375757e45a32", size = 10220762, upload-time = "2026-09-03T16:56:44.681Z" }, + { url = "https://files.pythonhosted.org/packages/43/d9/b75668ce41e4c8d073d18d6d08672ba6906ce45d5c06ea4fdb2e84ce3853/ruff-0.16.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:99b62ea33baf130f50368798d841f0d95527b6d817bf31817b65dd058f1d314c", size = 9835082, upload-time = "2026-09-03T16:56:47.142Z" }, + { url = "https://files.pythonhosted.org/packages/99/97/123ab10b05cde889c107c20f5a9774955104b5552796a2a8584b089ae8eb/ruff-0.16.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fbf89013f2bb3f6835a6038ff658dc8a1b38c98dc8e724b964168ad4e881876", size = 9949304, upload-time = "2026-09-03T16:56:49.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/58/a4a2c59dd2e5b85929c912d9cac3056eb9ee8c7e75e9b9fe3e109174966b/ruff-0.16.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56a67065e22efa6bc4d498299d3bb06c0c90aace8fac2068b5a12f9dc4d8d51d", size = 9840612, upload-time = "2026-09-03T16:56:52.368Z" }, + { url = "https://files.pythonhosted.org/packages/61/6a/ff8c8626a786c4f49d48ced4a752dadbca65f5263005f9c2416578194694/ruff-0.16.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e25cc89174874b176a157e4428d66761c2c0c006654419bf384f967f361ff1b1", size = 10543465, upload-time = "2026-09-03T16:56:55.089Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bb/c47535923365f337b82e28192e4e9eef2176511007cfd99a62fc22df5dad/ruff-0.16.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700580ed5303723cb3c11c2f1d2a8913ce77b7ea86646dddb887f5417a9ba70", size = 11267576, upload-time = "2026-09-03T16:56:57.791Z" }, + { url = "https://files.pythonhosted.org/packages/ba/50/e5119a5212b5cd63b51e1f4b25e7bd636a6668fc069a3160b108ad7e3c16/ruff-0.16.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15f1d0b6e165a6e56567befb6629f8209271311d990bae0f37e6d065035ef5f3", size = 10781993, upload-time = "2026-09-03T16:57:00.666Z" }, + { url = "https://files.pythonhosted.org/packages/8b/98/083d8b4ef3c51a0d19db84367791cbe9f44e4b53343d19dfa83556e1cd9a/ruff-0.16.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d72c591a96986ee4268860e2b7235082129ca5e4cb9cbba653a4b57c11893757", size = 10317748, upload-time = "2026-09-03T16:57:03.428Z" }, + { url = "https://files.pythonhosted.org/packages/9a/29/68f7ff2c5ad95f19f00627ac2de95644e25fe47371ea60b2db1fd952315e/ruff-0.16.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:65a006baa18f33324325814c864daef03541d51564b98c517610ea756ab7003e", size = 10540096, upload-time = "2026-09-03T16:57:06.182Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f9/79a8f6de85968641d68a7863aeec577551924ef066a990a48ff93167beab/ruff-0.16.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cd02a7bf1a21a8735228a3e8c95a9dc5cf86bd2a52194f4aaae2a5755b4de0f4", size = 10100494, upload-time = "2026-09-03T16:57:09.194Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e8/b81a22d9b90c00b892ccf2fa2ac36fa95de4c13ab85aea3e73795cfe4651/ruff-0.16.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:31b36f1e5ad85e0737f09d2be4e512e2e283583c14015da3b9dc07359ac0fc88", size = 9843663, upload-time = "2026-09-03T16:57:12.168Z" }, + { url = "https://files.pythonhosted.org/packages/39/aa/54f516ec5e5a11c4afdceb1c454ebb054ffb96e4f4a1705580b4346abd35/ruff-0.16.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:61029b4ab4aa723fd3064fab96b1d814492596bf0c792679fffcbde1e1679953", size = 10282461, upload-time = "2026-09-03T16:57:15.077Z" }, + { url = "https://files.pythonhosted.org/packages/52/0b/38d0aa8aa32372b96dc44f97b22e576c4147808271aab7b2cb1e353d4445/ruff-0.16.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9ac8998457832c2061709d900856b7ad271dace0cb41f346588d540162bfa718", size = 10728808, upload-time = "2026-09-03T16:57:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e5/9e274e24eeb027640ffc7442f21239f16d17f47acec15ae34f32e03a5c79/ruff-0.16.6-py3-none-win32.whl", hash = "sha256:0b87d9d16fcb63e8018423ca1d50b7260f15cb2da33e30db4baad4183a948c25", size = 10049212, upload-time = "2026-09-03T16:57:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/22/31/72472449414223ed1a2da236b992adbb1a2ae59e34794574810f60ce068e/ruff-0.16.6-py3-none-win_amd64.whl", hash = "sha256:10d21c51c3495d8eaea7b703a16592117ea6eb1d649e36335aa965ff1173eb39", size = 10556402, upload-time = "2026-09-03T16:57:23.501Z" }, + { url = "https://files.pythonhosted.org/packages/fc/07/d781f8f8e1ac24bef9f3269cf62ffb1407ca24c3a8f12e5e22874f90528c/ruff-0.16.6-py3-none-win_arm64.whl", hash = "sha256:7a976c79b958f94e50a022a19f0f8c87387448020935ec14fc74331bd0a7f2c5", size = 10412850, upload-time = "2026-09-03T16:57:26.416Z" }, ] [[package]] @@ -4229,15 +4230,15 @@ asyncio = [ [[package]] name = "sse-starlette" -version = "3.4.10" +version = "3.4.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/e1/8a41e88e825ea26c44333897c7ffe35fe60153a2cfc097a5bd1d209ad281/sse_starlette-3.4.10.tar.gz", hash = "sha256:c6c87280d8feb4e55a8d79633782766b9cac6a26da5c79a145d00aa404117a86", size = 33720, upload-time = "2026-09-03T09:36:24.08Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/54/6767bb789b2f2fed6e0f953df949cd39dc263a384c1b65a95232598621d6/sse_starlette-3.4.11.tar.gz", hash = "sha256:1bae716c02f3e6f294be41ff333220692dae7c3cbab077c900f159676719dade", size = 34972, upload-time = "2026-09-05T12:11:04.607Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/3c/96018a51c7301a64f7b0579d9ce8f9b69dd39ca8ed5aa100ba3feadee503/sse_starlette-3.4.10-py3-none-any.whl", hash = "sha256:710f5f5b0527409903a22a91699db02f76f4c2eb9204e882e4ee7cada76bdf75", size = 17120, upload-time = "2026-09-03T09:36:22.56Z" }, + { url = "https://files.pythonhosted.org/packages/98/6a/2ba3ed4a69babf3afdddf7d8314a48d87562c0a442206bbc2a1b50d5efc0/sse_starlette-3.4.11-py3-none-any.whl", hash = "sha256:c7b2244bdff016fe7f64e10075e89a3e6bbf899649cc89b0fe884b5545042453", size = 17122, upload-time = "2026-09-05T12:11:03.195Z" }, ] [[package]] @@ -4588,23 +4589,23 @@ wheels = [ [[package]] name = "types-pyyaml" -version = "6.0.12.20260815" +version = "6.0.12.20260906" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9f/72/b56089aeee6c496d969bac42376bedb6e3eeab4682e1018fa3137122f94b/types_pyyaml-6.0.12.20260815.tar.gz", hash = "sha256:28764110c9cf35846e733da32d8d734df7473c5dde9ef67c3b7332ec0e819858", size = 18545, upload-time = "2026-08-15T02:41:51.532Z" } +sdist = { url = "https://files.pythonhosted.org/packages/90/6e/abec85b9013db5b934b0280a6dd104904d84f7bcbaab2e2f3def87ac7463/types_pyyaml-6.0.12.20260906.tar.gz", hash = "sha256:f59c1cc05010b833d2d72287bbaa72610106b28d42d89a907313117faba85212", size = 18649, upload-time = "2026-09-06T06:35:35.362Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/52/eefeba09be4ef2a1eb989eb92934561e8e502a6ee3c32654996e4be7e399/types_pyyaml-6.0.12.20260815-py3-none-any.whl", hash = "sha256:6f332212b7e191f3afd5016a713c510b6340593b7ebec573c7d5d20aa5386d3b", size = 21148, upload-time = "2026-08-15T02:41:50.555Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/fc0644b7ddcfb969e95845837143cb5173ddd6e06ee4ba5fc493cd9329b7/types_pyyaml-6.0.12.20260906-py3-none-any.whl", hash = "sha256:bca893ff0d51df5c9053137d5d0e6ccd36e939a196356f1d5c16372422f5137b", size = 21282, upload-time = "2026-09-06T06:35:34.372Z" }, ] [[package]] name = "types-requests" -version = "2.33.0.20260712" +version = "2.33.0.20260906" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/51/703318f7b7be8bee126ec13bf615050f932d0179b8784420f3a0199cc769/types_requests-2.33.0.20260712.tar.gz", hash = "sha256:2141b67ab534a5c5cd2dac5034f2a35f42e699c5bf185eee608c5246a069d7fb", size = 25084, upload-time = "2026-07-12T05:14:20.455Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/18/4c2c0290953f8b3b9612adfcb07b57f144ade3ad32a76764fca42b77c5f3/types_requests-2.33.0.20260906.tar.gz", hash = "sha256:76ab8a0fb736744a0c3deee7aa57b2927e301f078d9e61f5391b3e92002416b9", size = 25263, upload-time = "2026-09-06T06:35:47.707Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/e7/010c87f559e216d83f9dc51e939633fd0d0ead3377340181ab0e223cd3b5/types_requests-2.33.0.20260712-py3-none-any.whl", hash = "sha256:de027e28c171d3da529689cbfa023b0b4eab188c8dfa22fd834eebd2cee6e7bb", size = 21392, upload-time = "2026-07-12T05:14:19.616Z" }, + { url = "https://files.pythonhosted.org/packages/60/4c/51ec821d22a45b4162fa3f3e9e94ea4c0f8c49e82363b10955baa5438391/types_requests-2.33.0.20260906-py3-none-any.whl", hash = "sha256:9f53622652bd921ead7a54d665be1b8d518165c8b51cc9d68d944cd6b6bfa8fb", size = 21461, upload-time = "2026-09-06T06:35:45.856Z" }, ] [[package]] From ec41a7cea0152df11b0ede965c168d936ebca228 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Mon, 7 Sep 2026 08:40:42 +0200 Subject: [PATCH 017/120] LCORE-3298: Fixed issue --- scripts/vulnerability_report.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/vulnerability_report.py b/scripts/vulnerability_report.py index b5b539e51..4c8f93e8f 100644 --- a/scripts/vulnerability_report.py +++ b/scripts/vulnerability_report.py @@ -479,16 +479,16 @@ def generate_severity_graph( svg_output (bool): If true, save the graph as SVG. png_output (bool): If true, save the graph as PNG. """ - fig, ax = plt.subplots() - D = stat["severity"] + _, ax = plt.subplots() + data = stat["severity"] ax.bar( - range(len(D)), - list(D.values()), + range(len(data)), + list(data.values()), align="center", color=["#c00000", "orange", "#e0e000", "#00c000"], ) - ax.set_xticks(range(len(D)), list(D.keys())) - save_graph(fig, prefix, "severity", svg_output, png_output) + ax.set_xticks(range(len(data)), list(data.keys())) + save_graph(prefix, "severity", svg_output, png_output) def generate_vuln_for_days_graph( From 997f2bc5acc68bd05cdb67147bfb46d6113fcd57 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Mon, 7 Sep 2026 08:43:47 +0200 Subject: [PATCH 018/120] LCORE-3301: Fixed issue --- scripts/vulnerability_report.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/vulnerability_report.py b/scripts/vulnerability_report.py index b5b539e51..636adb0bd 100644 --- a/scripts/vulnerability_report.py +++ b/scripts/vulnerability_report.py @@ -526,14 +526,14 @@ def generate_vulnerable_packages_graph( png_output (bool): If true, save the graph as PNG. """ fig, ax = plt.subplots() - D = stat["packages"] - names, counts = zip(*D.most_common(10)) + data = stat["packages"] + names, counts = zip(*data.most_common(10)) ax.bar(names, counts, edgecolor="black") ax.set_ylim(top=100) ax.set_title("CVEs per package") ax.tick_params(axis="x", labelrotation=90) fig.tight_layout() - save_graph(fig, prefix, "packages", svg_output, png_output) + save_graph(prefix, "packages", svg_output, png_output) def generate_new_cve_dates_graph( From 807523553b85626797f3f557bb7423ce524c0deb Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Mon, 7 Sep 2026 08:50:50 +0200 Subject: [PATCH 019/120] LCORE-3367: Fixed imports --- scripts/konflux_resolve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/konflux_resolve.py b/scripts/konflux_resolve.py index 195745924..07ff4988b 100644 --- a/scripts/konflux_resolve.py +++ b/scripts/konflux_resolve.py @@ -13,10 +13,10 @@ import os import re import subprocess +import sys import time import tomllib import urllib.request -import sys from collections import deque from collections.abc import Sequence from html.parser import HTMLParser From 4ff6635ded4952af62649445e709f2760013d2e7 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Mon, 7 Sep 2026 08:53:55 +0200 Subject: [PATCH 020/120] LCORE-3367: Simpler loop --- scripts/konflux_resolve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/konflux_resolve.py b/scripts/konflux_resolve.py index 195745924..79e3486a5 100644 --- a/scripts/konflux_resolve.py +++ b/scripts/konflux_resolve.py @@ -890,7 +890,7 @@ def write_hashed_requirements( # RHOAI packages store hashes per platform if "platforms" in info: - for _arch, (_, sha) in info["platforms"].items(): + for (_, sha) in info["platforms"].values(): if sha: hashes.add(sha) From 47f0eb2228cde45159d3bf97c7afd5250b04e14a Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Mon, 7 Sep 2026 12:07:50 +0200 Subject: [PATCH 021/120] LCORE-3819: Debug log to figure out path issues --- src/app/main.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/app/main.py b/src/app/main.py index 5f2476cd6..bafc74d63 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -222,6 +222,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: # Ignore paths that are not part of the app routes. if path not in app_routes_paths: + logger.debug("Ignoring path: %s", path) await self.app(scope, receive, send) return @@ -300,6 +301,11 @@ async def send_wrapper(message: Message) -> None: and rc.original_route.path # pyright: ignore[reportAttributeAccessIssue] ] +logger.debug("Route paths:") +for app_routes_path in app_routes_paths: + logger.debug(app_routes_path) + + # Register pure ASGI middlewares. Middleware execution order is the reverse of # registration order: GlobalExceptionMiddleware (registered first) is innermost, # RestApiMetricsMiddleware (registered last) is outermost. This ensures metrics From 47a3d0bad5a1077155a87a2388f511a4f1d5d2e2 Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Mon, 7 Sep 2026 13:45:03 +0200 Subject: [PATCH 022/120] LCORE-3963: fix SC2140 in fetch-jira.sh embedded Python MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ADF extractor is passed to python3 via -c inside a double-quoted shell string, so every double quote in that block is a shell quote, not a Python one. The comment explaining the link-mark handling contained the literal "here", which closed the shell string and reopened it around the word. The script happened to still work — the concatenated words reassemble into the same argument — but shellcheck flags the pattern as SC2140 ("Word is of the form \"A\"B\"C\"") and the CI shellcheck job failed on it. Use single quotes in the comment instead. Single quotes carry no meaning inside a double-quoted shell string and none inside a Python comment, so the fix is confined to the comment text and leaves the extractor's behavior untouched. Verified with the same command CI runs, shellcheck -- */*.sh, which now passes clean, and by fetching LCORE-3963 end to end to confirm the ADF rendering is unchanged. --- dev-tools/fetch-jira.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-tools/fetch-jira.sh b/dev-tools/fetch-jira.sh index 549045e80..8dea83c67 100755 --- a/dev-tools/fetch-jira.sh +++ b/dev-tools/fetch-jira.sh @@ -149,7 +149,7 @@ def extract_text(node, depth=0): text = f'\`{text}\`' elif m.get('type') == 'link': # Keep the target: a link whose text differs from its - # href (ticket keys, "here", PR titles) is otherwise lost. + # href (ticket keys, 'here', PR titles) is otherwise lost. href = m.get('attrs', {}).get('href', '') if href and href != text: text = text + ' <' + href + '>' From 125bb54d968ac5befaec84c3274aa93808cfa906 Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Mon, 7 Sep 2026 13:48:43 +0200 Subject: [PATCH 023/120] LCORE-3963: correct blockquote, inlineCard and mark-order handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Testing the new ADF branches against a synthetic document that exercises each of them turned up three defects in the extractor added earlier on this branch. Multi-paragraph blockquotes were run together. The branch joined its children with '', but extract_text returns one entry per line, so two quoted paragraphs came back as a single concatenated line — output the pre-existing code, which had no blockquote branch at all, got right by accident. Join on a newline instead and prefix each resulting line, emitting a bare '>' for the blank line between paragraphs rather than a '> ' with trailing whitespace. An empty blockquote now yields nothing instead of a stray quote marker. inlineCard nodes without a top-level url attribute rendered as an empty '<>'. Jira puts the target in attrs.url for an unresolved smart link but in the resolved JSON-LD attrs.data.url once the card has been fetched, so fall back to that and emit nothing when neither is present. Link marks combined with a code or strong mark placed the URL inside the markup, producing `run_it ` with the target swallowed by the code span, and the result depended on the order Jira happened to serialise the marks in. Collect the href during the loop and append it after all other marks have been applied, which makes the rendering deterministic. Compare the href against the raw node text rather than the marked-up text so an autolinked bare URL carrying an additional mark is still recognised as its own target and not duplicated. Verified with a synthetic ADF document covering link, inlineCard, mention, rule, blockquote and table nodes, diffed against the same document rendered by the pre-branch extractor to confirm every remaining difference is an intended improvement, and end to end against LCORE-3788 (smart links), LCORE-3883 --comments (code blocks and lists in the comment thread) and --linked-depth 1. shellcheck -- */*.sh passes. --- dev-tools/fetch-jira.sh | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/dev-tools/fetch-jira.sh b/dev-tools/fetch-jira.sh index 8dea83c67..86c716c7a 100755 --- a/dev-tools/fetch-jira.sh +++ b/dev-tools/fetch-jira.sh @@ -142,21 +142,30 @@ def extract_text(node, depth=0): if ntype == 'text': text = node.get('text', '') marks = node.get('marks', []) + href = '' for m in marks: if m.get('type') == 'strong': text = f'**{text}**' elif m.get('type') == 'code': text = f'\`{text}\`' elif m.get('type') == 'link': - # Keep the target: a link whose text differs from its - # href (ticket keys, 'here', PR titles) is otherwise lost. href = m.get('attrs', {}).get('href', '') - if href and href != text: - text = text + ' <' + href + '>' + # Keep the target: a link whose text differs from its href + # (ticket keys, 'here', PR titles) is otherwise lost. Appended + # after the other marks so the URL never lands inside a code + # span or bold run, whatever order the marks arrived in, and + # compared against the raw text so an autolinked bare URL that + # also carries a mark is still recognised as its own target. + if href and href != node.get('text', ''): + text = text + ' <' + href + '>' return [text] if ntype == 'inlineCard': - # Smart links (pasted Jira/GitHub URLs) carry the URL only here. - return ['<' + node.get('attrs', {}).get('url', '') + '>'] + # Smart links (pasted Jira/GitHub URLs) carry the URL only here, + # either directly or inside the resolved JSON-LD 'data' blob. + attrs = node.get('attrs', {}) + data = attrs.get('data') + url = attrs.get('url') or (data.get('url', '') if isinstance(data, dict) else '') + return ['<' + url + '>'] if url else [] if ntype == 'mention': return [node.get('attrs', {}).get('text', '@?')] if ntype == 'hardBreak': @@ -187,7 +196,12 @@ def extract_text(node, depth=0): child_text = [] for c in node.get('content', []): child_text.extend(extract_text(c, depth)) - return ['> ' + l for l in ''.join(child_text).strip().split('\n')] + # Children already come back one line each; joining on '' would + # run every paragraph of the quote together into a single line. + quoted = '\n'.join(child_text).strip() + if not quoted: + return [] + return ['> ' + ln if ln else '>' for ln in quoted.split('\n')] if ntype == 'table': rows = [] for row in node.get('content', []): From 7eb0214073adfd38cd53a73cf790ae9bc0e3efda Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Mon, 7 Sep 2026 13:54:36 +0200 Subject: [PATCH 024/120] LCORE-3963: keep inline runs on one line, strip Jira tracking fragments Three follow-ups to the ADF extractor, all in the rendering of a single paragraph. Inline runs were split across lines. extract_text returns one entry per line and the generic block walk extends that list for every child, so a paragraph made of several inline nodes came out one node per line. A sentence like "See PR #2451 and here." rendered as five separate lines, and adding link targets earlier on this branch made the effect more visible rather than less, since the target is only useful next to the text it belongs to. Give paragraph its own branch that concatenates its inline children into one line and lets an explicit hardBreak be the only thing that splits it. The paragraph tail in the generic walk becomes unreachable and is removed. Jira appends an '#icft=KEY' tracking fragment to internal smart links and issue mentions, so every reference to another ticket rendered as ''. The fragment is Jira's own click tracking, never part of the target. Strip it in one helper applied to both URL sites, the link mark and the inlineCard. Tables rendered as bare pipe-separated lines with no rule under the header row, which is not valid markdown. Detect a header row by its tableHeader cells and emit the rule beneath it, and follow the table with a blank line so it does not run into the next paragraph. Cell contents are collapsed onto a single line, joining on a newline first so a cell holding two paragraphs keeps a space between them instead of running the words together; without this a multi-paragraph cell injected a newline into the middle of a row and broke the alignment of the whole table. Verified against a synthetic ADF document covering link marks with and without a tracking fragment, inlineCard, mention, hardBreak, nested list items, multi-paragraph blockquotes, and a table with a header row and a multi-paragraph cell. End to end against LCORE-3788, whose description is now readable prose with clean ticket URLs where it was previously one fragment per line, LCORE-3883 --comments to confirm code blocks and lists in comment threads are unaffected, and --linked-depth 1. shellcheck -- */*.sh passes. --- dev-tools/fetch-jira.sh | 43 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/dev-tools/fetch-jira.sh b/dev-tools/fetch-jira.sh index 86c716c7a..33bd1bbe4 100755 --- a/dev-tools/fetch-jira.sh +++ b/dev-tools/fetch-jira.sh @@ -133,6 +133,13 @@ if parent_key: print() +# Jira appends its own '#icft=KEY' tracking fragment to internal smart +# links and issue mentions. It is never part of the target and only adds +# noise to the rendered line, so drop it from every URL we print. +def clean_url(url): + return url.split('#icft=')[0] if '#icft=' in url else url + + # ADF (Atlassian Document Format) → markdown-ish text extractor. # Hoisted to top-level so both description and comments can use it. def extract_text(node, depth=0): @@ -156,6 +163,7 @@ def extract_text(node, depth=0): # span or bold run, whatever order the marks arrived in, and # compared against the raw text so an autolinked bare URL that # also carries a mark is still recognised as its own target. + href = clean_url(href) if href and href != node.get('text', ''): text = text + ' <' + href + '>' return [text] @@ -165,6 +173,7 @@ def extract_text(node, depth=0): attrs = node.get('attrs', {}) data = attrs.get('data') url = attrs.get('url') or (data.get('url', '') if isinstance(data, dict) else '') + url = clean_url(url) return ['<' + url + '>'] if url else [] if ntype == 'mention': return [node.get('attrs', {}).get('text', '@?')] @@ -172,6 +181,20 @@ def extract_text(node, depth=0): return ['\n'] if ntype == 'rule': return ['---'] + if ntype == 'paragraph': + # A paragraph's children are inline runs — text, smart links, + # mentions. The generic block walk below puts every child on its + # own line, which chops any sentence containing a link into + # fragments, so join them into one flowing line instead and let + # an explicit hardBreak be the only thing that splits it. + joined = ''.join( + piece + for c in node.get('content', []) + for piece in extract_text(c, depth) + ) + if not joined.strip(): + return [] + return joined.split('\n') + [''] if ntype == 'listItem': child_text = [] for c in node.get('content', []): @@ -206,17 +229,29 @@ def extract_text(node, depth=0): rows = [] for row in node.get('content', []): cells = [] + header = False for cell in row.get('content', []): + if cell.get('type') == 'tableHeader': + header = True cell_text = [] for c in cell.get('content', []): cell_text.extend(extract_text(c, depth)) - cells.append(''.join(cell_text).strip()) + # Collapse to a single line: a cell holding two + # paragraphs would otherwise inject a newline into the + # middle of the row and break the whole table. Join on a + # newline first so the paragraph boundary survives as the + # space that separates them. + cells.append(' '.join('\n'.join(cell_text).split())) + if not cells: + continue rows.append(' | '.join(cells)) - return rows + # Emit the markdown rule under a header row so the result is + # a real table rather than pipe-separated lines. + if header and len(rows) == 1: + rows.append(' | '.join(['---'] * len(cells))) + return rows + [''] if rows else [] for c in node.get('content', []): lines.extend(extract_text(c, depth)) - if ntype == 'paragraph' and lines: - lines.append('') return lines From 8cd98348f8107f44280f908facf0a116eba3b24c Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Mon, 7 Sep 2026 13:57:40 +0200 Subject: [PATCH 025/120] LCORE-3963: do not abandon a run when one ticket cannot be fetched The script runs under 'set -euo pipefail', which defeated its own error handling in three places. The main issue fetch assigned from an unguarded curl, so a connection failure aborted the script before the 'Error fetching' branch below it could run. That branch was effectively dead for anything other than an HTTP error with a JSON body, since curl reports 4xx and 5xx through the response rather than its exit code. The two JQL child lookups had the same problem one level worse: they pipe curl into python3, and pipefail turns a curl failure into a failed assignment. fetch_ticket then returns 1 after reporting a failure, and every call site was unguarded, so the first unreachable ticket ended the whole run. Fetching two keys printed one error and stopped; a relation that could not be fetched during recursion abandoned its siblings. Guard the three assignments, tolerate a failed relation during recursion, and accumulate failures at the top level so that every requested ticket is attempted and reported while the script still exits non-zero when any of them failed. Verified by pointing the configured instance at a closed port: fetching two tickets now reports both and exits 1, where it previously printed a single error and exited on the first. Re-checked against the live instance that two-ticket and --linked-depth 1 runs are unaffected and still exit 0. --- dev-tools/fetch-jira.sh | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/dev-tools/fetch-jira.sh b/dev-tools/fetch-jira.sh index 33bd1bbe4..13e753ffa 100755 --- a/dev-tools/fetch-jira.sh +++ b/dev-tools/fetch-jira.sh @@ -97,10 +97,14 @@ fetch_ticket() { esac FETCHED_KEYS="$FETCHED_KEYS$key " + # The '|| data=' guard matters: under 'set -e' an unguarded assignment + # from a failing curl aborts the whole script, so a single unreachable + # ticket would kill a multi-ticket or recursive run instead of falling + # through to the 'Error fetching' branch below and carrying on. local data data=$(curl -sS --connect-timeout 10 --max-time 30 \ -u "$JIRA_EMAIL:$JIRA_TOKEN" \ - "$JIRA_INSTANCE/rest/api/3/issue/$key?fields=summary,status,issuetype,description,issuelinks,subtasks,parent" 2>/dev/null) + "$JIRA_INSTANCE/rest/api/3/issue/$key?fields=summary,status,issuetype,description,issuelinks,subtasks,parent" 2>/dev/null) || data='' # Optional: fetch comments (only if --comments was passed). Empty # JSON object signals "no comments fetched" to the Python printer. @@ -361,19 +365,26 @@ try: print(issue['key']) except Exception: pass -" 2>/dev/null | tr '\n' ' ') +" 2>/dev/null | tr '\n' ' ') || jql_kids='' local rk for rk in $related_keys $jql_kids; do [ -z "$rk" ] && continue echo - fetch_ticket "$rk" "${indent} " $((depth - 1)) + # A relation we cannot fetch is reported by fetch_ticket and + # then tolerated: it must not abandon the rest of the recursion. + fetch_ticket "$rk" "${indent} " $((depth - 1)) || true done fi } +# Every requested ticket is attempted even when an earlier one fails, so +# one unreachable key does not hide the rest of the output; the script +# still exits non-zero if any of them failed. +EXIT_STATUS=0 + # Fetch main ticket (with depth recursion if requested) -fetch_ticket "$TICKET" "" "$LINKED_DEPTH" +fetch_ticket "$TICKET" "" "$LINKED_DEPTH" || EXIT_STATUS=1 # At depth 0, also list JQL parent= children as a flat summary (legacy # behavior — useful as a quick "what's underneath" overview without @@ -395,7 +406,7 @@ try: print(f'{key} ({itype}) [{status}]: {summary}') except Exception: pass -" 2>/dev/null) +" 2>/dev/null) || CHILD_KEYS='' if [ -n "$CHILD_KEYS" ]; then echo "Child issues:" @@ -414,5 +425,7 @@ for extra in "$@"; do fi echo "────────────────────────────────────────────────────────" echo "" - fetch_ticket "$extra" "" "$LINKED_DEPTH" + fetch_ticket "$extra" "" "$LINKED_DEPTH" || EXIT_STATUS=1 done + +exit "$EXIT_STATUS" From ebc93fa9ccf93d35a7febf4bf195d84763a4f73c Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Mon, 7 Sep 2026 14:13:14 +0200 Subject: [PATCH 026/120] LCORE-3963: hold the embedded Python in heredocs instead of quoted strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All four Python programs in this script were passed inline to 'python3 -c', which puts them inside a double-quoted shell word. Every quote, dollar sign and backtick in roughly two hundred lines of Python was therefore shell syntax first and Python second. That is not a hypothetical: a comment in the extractor containing the word "here" in double quotes closed the shell string and reopened it, which shellcheck reported as SC2140 and CI failed on. The code-span rendering needed backslash-escaped backticks for the same reason, and any future edit introducing an f-string with a dollar sign, or a docstring, would have been silently mangled rather than rejected. Hoist each program into a quoted heredoc and pass the resulting variable. A <<'EOF' heredoc is taken verbatim, so the Python is now written exactly as Python, with no escaping, and the whole class of quoting bug is gone. The two backslash-escaped backticks are unescaped as part of the move. This is a pure move: no logic changed. Verified by capturing the output of four invocations before the change — LCORE-3788, LCORE-3883 with --comments, LCORE-3788 with --linked-depth 1, and a two-ticket run — and diffing them against the same four afterwards. All four are byte identical. The error path was re-checked against a closed port and still reports both tickets and exits 1. --- dev-tools/fetch-jira.sh | 150 +++++++++++++++++++++++----------------- 1 file changed, 85 insertions(+), 65 deletions(-) diff --git a/dev-tools/fetch-jira.sh b/dev-tools/fetch-jira.sh index 13e753ffa..d1ed18882 100755 --- a/dev-tools/fetch-jira.sh +++ b/dev-tools/fetch-jira.sh @@ -86,37 +86,15 @@ fi # leading and trailing spaces so substring matching works cleanly). FETCHED_KEYS=" " -fetch_ticket() { - local key="$1" - local indent="${2:-}" - local depth="${3:-0}" - - # Cycle / dup protection - case "$FETCHED_KEYS" in - *" $key "*) return 0 ;; - esac - FETCHED_KEYS="$FETCHED_KEYS$key " - - # The '|| data=' guard matters: under 'set -e' an unguarded assignment - # from a failing curl aborts the whole script, so a single unreachable - # ticket would kill a multi-ticket or recursive run instead of falling - # through to the 'Error fetching' branch below and carrying on. - local data - data=$(curl -sS --connect-timeout 10 --max-time 30 \ - -u "$JIRA_EMAIL:$JIRA_TOKEN" \ - "$JIRA_INSTANCE/rest/api/3/issue/$key?fields=summary,status,issuetype,description,issuelinks,subtasks,parent" 2>/dev/null) || data='' - - # Optional: fetch comments (only if --comments was passed). Empty - # JSON object signals "no comments fetched" to the Python printer. - local comments_data='{}' - if [ "$FETCH_COMMENTS" -eq 1 ]; then - comments_data=$(curl -sS --connect-timeout 10 --max-time 30 \ - -u "$JIRA_EMAIL:$JIRA_TOKEN" \ - "$JIRA_INSTANCE/rest/api/3/issue/$key/comment" 2>/dev/null) || comments_data='{}' - fi - - if echo "$data" | python3 -c "import sys,json; json.load(sys.stdin)['key']" >/dev/null 2>&1; then - python3 -c " +# The Python programs below are held in quoted heredocs rather than +# passed inline to 'python3 -c'. An inline program sits inside a +# double-quoted shell word, which makes every quote, dollar sign and +# backtick in it shell syntax first and Python second. That is what broke +# this file with an SC2140 warning, and it stays a hazard for anyone +# editing the extractor. A <<'EOF' heredoc is passed through verbatim, so +# the Python can be written exactly as Python. + +PRINT_TICKET_PY=$(cat <<'PYEOF_PRINT_TICKET_PY' import json, sys, textwrap data = json.loads(sys.argv[1]) @@ -158,7 +136,7 @@ def extract_text(node, depth=0): if m.get('type') == 'strong': text = f'**{text}**' elif m.get('type') == 'code': - text = f'\`{text}\`' + text = f'`{text}`' elif m.get('type') == 'link': href = m.get('attrs', {}).get('href', '') # Keep the target: a link whose text differs from its href @@ -218,7 +196,7 @@ def extract_text(node, depth=0): child_text = [] for c in node.get('content', []): child_text.extend(extract_text(c, depth)) - return ['\`\`\`\n' + ''.join(child_text) + '\n\`\`\`'] + return ['```\n' + ''.join(child_text) + '\n```'] if ntype == 'blockquote': child_text = [] for c in node.get('content', []): @@ -323,18 +301,10 @@ if comments: for line in text.split('\n'): print(f'{indent} {line}') print() -" "$data" "$indent" "$comments_data" - else - echo "${indent}Error fetching $key" - echo "$data" | head -3 - return 1 - fi +PYEOF_PRINT_TICKET_PY +) - # Recurse into related tickets if depth > 0 - if [ "$depth" -gt 0 ]; then - # Extract subtask + linked-issue keys from already-fetched data - local related_keys - related_keys=$(echo "$data" | python3 -c " +RELATED_KEYS_PY=$(cat <<'PYEOF_RELATED_KEYS_PY' import json, sys try: d = json.load(sys.stdin) @@ -350,14 +320,10 @@ try: print(' '.join(out)) except Exception: pass -" 2>/dev/null) +PYEOF_RELATED_KEYS_PY +) - # Also fetch JQL parent= children - local jql_kids - jql_kids=$(curl -sS --connect-timeout 10 --max-time 30 \ - -u "$JIRA_EMAIL:$JIRA_TOKEN" \ - "$JIRA_INSTANCE/rest/api/3/search/jql?jql=parent%3D${key}&fields=key&maxResults=20" 2>/dev/null | \ - python3 -c " +JQL_KIDS_PY=$(cat <<'PYEOF_JQL_KIDS_PY' import json, sys try: d = json.load(sys.stdin) @@ -365,7 +331,73 @@ try: print(issue['key']) except Exception: pass -" 2>/dev/null | tr '\n' ' ') || jql_kids='' +PYEOF_JQL_KIDS_PY +) + +CHILD_KEYS_PY=$(cat <<'PYEOF_CHILD_KEYS_PY' +import json, sys +try: + data = json.load(sys.stdin) + for issue in data.get('issues', []): + key = issue['key'] + summary = issue['fields']['summary'] + status = issue['fields']['status']['name'] + itype = issue['fields']['issuetype']['name'] + print(f'{key} ({itype}) [{status}]: {summary}') +except Exception: + pass +PYEOF_CHILD_KEYS_PY +) + +fetch_ticket() { + local key="$1" + local indent="${2:-}" + local depth="${3:-0}" + + # Cycle / dup protection + case "$FETCHED_KEYS" in + *" $key "*) return 0 ;; + esac + FETCHED_KEYS="$FETCHED_KEYS$key " + + # The '|| data=' guard matters: under 'set -e' an unguarded assignment + # from a failing curl aborts the whole script, so a single unreachable + # ticket would kill a multi-ticket or recursive run instead of falling + # through to the 'Error fetching' branch below and carrying on. + local data + data=$(curl -sS --connect-timeout 10 --max-time 30 \ + -u "$JIRA_EMAIL:$JIRA_TOKEN" \ + "$JIRA_INSTANCE/rest/api/3/issue/$key?fields=summary,status,issuetype,description,issuelinks,subtasks,parent" 2>/dev/null) || data='' + + # Optional: fetch comments (only if --comments was passed). Empty + # JSON object signals "no comments fetched" to the Python printer. + local comments_data='{}' + if [ "$FETCH_COMMENTS" -eq 1 ]; then + comments_data=$(curl -sS --connect-timeout 10 --max-time 30 \ + -u "$JIRA_EMAIL:$JIRA_TOKEN" \ + "$JIRA_INSTANCE/rest/api/3/issue/$key/comment" 2>/dev/null) || comments_data='{}' + fi + + if echo "$data" | python3 -c "import sys,json; json.load(sys.stdin)['key']" >/dev/null 2>&1; then + python3 -c "$PRINT_TICKET_PY" "$data" "$indent" "$comments_data" + else + echo "${indent}Error fetching $key" + echo "$data" | head -3 + return 1 + fi + + # Recurse into related tickets if depth > 0 + if [ "$depth" -gt 0 ]; then + # Extract subtask + linked-issue keys from already-fetched data + local related_keys + related_keys=$(echo "$data" | python3 -c "$RELATED_KEYS_PY" 2>/dev/null) + + # Also fetch JQL parent= children + local jql_kids + jql_kids=$(curl -sS --connect-timeout 10 --max-time 30 \ + -u "$JIRA_EMAIL:$JIRA_TOKEN" \ + "$JIRA_INSTANCE/rest/api/3/search/jql?jql=parent%3D${key}&fields=key&maxResults=20" 2>/dev/null | \ + python3 -c "$JQL_KIDS_PY" 2>/dev/null | tr '\n' ' ') || jql_kids='' local rk for rk in $related_keys $jql_kids; do @@ -394,19 +426,7 @@ if [ "$LINKED_DEPTH" -eq 0 ]; then CHILD_KEYS=$(curl -sS --connect-timeout 10 --max-time 30 \ -u "$JIRA_EMAIL:$JIRA_TOKEN" \ "$JIRA_INSTANCE/rest/api/3/search/jql?jql=parent%3D${TICKET}&fields=key,summary,status,issuetype&maxResults=20" 2>/dev/null | \ - python3 -c " -import json, sys -try: - data = json.load(sys.stdin) - for issue in data.get('issues', []): - key = issue['key'] - summary = issue['fields']['summary'] - status = issue['fields']['status']['name'] - itype = issue['fields']['issuetype']['name'] - print(f'{key} ({itype}) [{status}]: {summary}') -except Exception: - pass -" 2>/dev/null) || CHILD_KEYS='' + python3 -c "$CHILD_KEYS_PY" 2>/dev/null) || CHILD_KEYS='' if [ -n "$CHILD_KEYS" ]; then echo "Child issues:" From d0a28b2eeab47f0a8dce0c2bc8da79311f749adf Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Mon, 7 Sep 2026 14:16:03 +0200 Subject: [PATCH 027/120] LCORE-3963: render the remaining ADF node types Closes the rest of the gap the ticket describes. Everything below was being dropped or flattened, and each one loses information a reader of the ticket needs. Status lozenges, emoji and dates were dropped outright, so a line reading "deploy is IN PROGRESS as of " rendered as "deploy is as of". Attachments were dropped with no trace, so a ticket arguing from a screenshot read as if it argued from nothing; they are now named. Task items lost their checkbox, making a done item and an open one identical. Panels lost their type, so an info note and a warning read the same. The em and strike marks were ignored while strong and code were honoured. Ordered lists rendered with bullets, discarding the numbering the author chose; the marker moves from the list item to the enclosing list, which is the only place that knows whether an item needs a bullet or a number, and honours the list's start attribute. List items holding two paragraphs ran the last word of one into the first word of the next, the same defect already fixed for table cells and fixed the same way. Joining the rendered description then called strip(), which also removed the indent from its first line, so a description opening with a list had an unindented first item and correctly indented ones after it. Strip only newlines. Verified against a synthetic document covering every node type added here, and by diffing four live invocations against output captured before the change. LCORE-3883 with --comments is byte identical. LCORE-3788 differs only in its two ordered lists, which now number 1..2 and 1..4 where they previously showed bullets; its description does contain two orderedList nodes alongside two bulletList nodes, and the bulleted ones are unchanged, so the new output matches what the author wrote. --- dev-tools/fetch-jira.sh | 86 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 4 deletions(-) diff --git a/dev-tools/fetch-jira.sh b/dev-tools/fetch-jira.sh index d1ed18882..d43acd274 100755 --- a/dev-tools/fetch-jira.sh +++ b/dev-tools/fetch-jira.sh @@ -95,7 +95,7 @@ FETCHED_KEYS=" " # the Python can be written exactly as Python. PRINT_TICKET_PY=$(cat <<'PYEOF_PRINT_TICKET_PY' -import json, sys, textwrap +import datetime, json, sys, textwrap data = json.loads(sys.argv[1]) indent = sys.argv[2] @@ -137,6 +137,10 @@ def extract_text(node, depth=0): text = f'**{text}**' elif m.get('type') == 'code': text = f'`{text}`' + elif m.get('type') == 'em': + text = f'_{text}_' + elif m.get('type') == 'strike': + text = f'~~{text}~~' elif m.get('type') == 'link': href = m.get('attrs', {}).get('href', '') # Keep the target: a link whose text differs from its href @@ -159,6 +163,36 @@ def extract_text(node, depth=0): return ['<' + url + '>'] if url else [] if ntype == 'mention': return [node.get('attrs', {}).get('text', '@?')] + if ntype == 'emoji': + attrs = node.get('attrs', {}) + return [attrs.get('text') or attrs.get('shortName') or ''] + if ntype == 'status': + # A status lozenge carries meaning no other node repeats, so a + # ticket saying a step is DONE reads as empty without this. + label = node.get('attrs', {}).get('text', '') + return ['[' + label + ']'] if label else [] + if ntype == 'date': + timestamp = node.get('attrs', {}).get('timestamp', '') + if not timestamp: + return [] + try: + moment = datetime.datetime.fromtimestamp( + int(timestamp) / 1000, datetime.timezone.utc + ) + return [moment.strftime('%Y-%m-%d')] + except (ValueError, TypeError, OverflowError, OSError): + return [str(timestamp)] + if ntype in ('media', 'mediaInline'): + # Attachments have no text of their own; name them so a ticket + # that argues from a screenshot does not read as if it argued + # from nothing. + attrs = node.get('attrs', {}) + name = attrs.get('alt') or attrs.get('id') or '' + return ['[attachment: ' + name + ']'] if name else ['[attachment]'] + if ntype in ('mediaSingle', 'mediaGroup'): + for c in node.get('content', []): + lines.extend(extract_text(c, depth)) + return lines if ntype == 'hardBreak': return ['\n'] if ntype == 'rule': @@ -181,11 +215,39 @@ def extract_text(node, depth=0): child_text = [] for c in node.get('content', []): child_text.extend(extract_text(c, depth)) - return [' ' * depth + '- ' + ''.join(child_text).strip()] + # Join on a newline before collapsing so an item holding two + # paragraphs keeps a space between them instead of running the + # last word of one into the first word of the next. The marker + # is added by the enclosing list, which is the only place that + # knows whether the item needs a bullet or a number. + return [' '.join('\n'.join(child_text).split())] if ntype in ('bulletList', 'orderedList'): + ordered = ntype == 'orderedList' + first = node.get('attrs', {}).get('order', 1) if ordered else 1 + try: + first = int(first) + except (ValueError, TypeError): + first = 1 + for number, c in enumerate(node.get('content', []), start=first): + for item in extract_text(c, depth + 1): + if not item: + continue + marker = f'{number}. ' if ordered else '- ' + lines.append(' ' * (depth + 1) + marker + item) + return lines + if ntype == 'taskList': for c in node.get('content', []): lines.extend(extract_text(c, depth + 1)) return lines + if ntype == 'taskItem': + # Without the box a done item and an open one render identically. + child_text = [] + for c in node.get('content', []): + child_text.extend(extract_text(c, depth)) + body = ' '.join('\n'.join(child_text).split()) + state = node.get('attrs', {}).get('state', 'TODO') + box = '[x]' if state == 'DONE' else '[ ]' + return [' ' * depth + '- ' + box + ' ' + body] if body else [] if ntype == 'heading': level = node.get('attrs', {}).get('level', 1) child_text = [] @@ -197,6 +259,19 @@ def extract_text(node, depth=0): for c in node.get('content', []): child_text.extend(extract_text(c, depth)) return ['```\n' + ''.join(child_text) + '\n```'] + if ntype == 'panel': + # Jira panels carry their severity in the attrs, not the text, + # so an info note and a warning read the same without this. + panel_type = node.get('attrs', {}).get('panelType', 'info') + child_text = [] + for c in node.get('content', []): + child_text.extend(extract_text(c, depth)) + body = '\n'.join(child_text).strip() + if not body: + return [] + body_lines = body.split('\n') + head = '**[' + panel_type.upper() + ']** ' + body_lines[0] + return [head] + body_lines[1:] + [''] if ntype == 'blockquote': child_text = [] for c in node.get('content', []): @@ -241,7 +316,10 @@ def extract_text(node, depth=0): desc = fields.get('description') if desc and isinstance(desc, dict): text_lines = extract_text(desc) - desc_text = '\n'.join(text_lines).strip() + # Strip newlines only: a plain strip() would also eat the indent of the + # first line, so a description opening with a list lost its leading + # bullet indent while every later item kept it. + desc_text = '\n'.join(text_lines).strip('\n') if desc_text: for line in desc_text.split('\n'): print(f'{indent}{line}') @@ -291,7 +369,7 @@ if comments: if isinstance(body, dict): # Reuse the same ADF extractor used for descriptions. text_lines = extract_text(body) - text = '\n'.join(text_lines).strip() + text = '\n'.join(text_lines).strip('\n') if not text: text = '(comment body in ADF format; no text extracted)' elif isinstance(body, str): From 2babd58f48421e310bceff5a4f139993d2121c0a Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Mon, 7 Sep 2026 14:50:00 +0200 Subject: [PATCH 028/120] LCORE-3963: render block smart links and report truncated results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A second pass over the node types turned up the block forms of the smart link, blockCard and embedCard, which were still dropped entirely. These are what Jira produces when a URL is pasted on a line of its own, which is the ordinary way a ticket references a PR or another ticket, so this is the same loss the ticket was filed about and arguably the more common half of it. They resolve their URL exactly as inlineCard does and now share its branch. Also handled: expand and nestedExpand, whose title is usually the only description of the collapsed block and is exactly what an author writes when hiding a long log; decisionItem, which rendered as an unmarked line indistinguishable from prose; and the codeBlock language attribute, so a fenced block is tagged. Separately, two API results were being truncated in silence. The comment endpoint returns at most a hundred per page and the child search asks for twenty, and in both cases a partial answer was presented exactly like a complete one — the worst shape for a tool whose output is read as though it were the whole ticket. Neither is worth paginating for a dev script, but both responses say when they are partial, so report it: the comment header now reads 'N of TOTAL; rest not fetched' when the thread is longer than the page, and the child listing appends a line when the search reports another page exists. Verified that the four live invocations are unchanged from the previous commit, that a synthetic document exercising every newly handled type renders, and that both truncation notices appear when the response says the result is partial and stay absent when it does not. --- dev-tools/fetch-jira.sh | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/dev-tools/fetch-jira.sh b/dev-tools/fetch-jira.sh index d43acd274..8182367ba 100755 --- a/dev-tools/fetch-jira.sh +++ b/dev-tools/fetch-jira.sh @@ -153,9 +153,12 @@ def extract_text(node, depth=0): if href and href != node.get('text', ''): text = text + ' <' + href + '>' return [text] - if ntype == 'inlineCard': + if ntype in ('inlineCard', 'blockCard', 'embedCard'): # Smart links (pasted Jira/GitHub URLs) carry the URL only here, # either directly or inside the resolved JSON-LD 'data' blob. + # blockCard and embedCard are the block forms Jira produces when + # a URL is pasted on a line of its own, which is how most + # tickets reference a PR or another ticket. attrs = node.get('attrs', {}) data = attrs.get('data') url = attrs.get('url') or (data.get('url', '') if isinstance(data, dict) else '') @@ -255,10 +258,32 @@ def extract_text(node, depth=0): child_text.extend(extract_text(c, depth)) return ['#' * level + ' ' + ''.join(child_text).strip()] if ntype == 'codeBlock': + language = node.get('attrs', {}).get('language', '') or '' child_text = [] for c in node.get('content', []): child_text.extend(extract_text(c, depth)) - return ['```\n' + ''.join(child_text) + '\n```'] + return ['```' + language + '\n' + ''.join(child_text) + '\n```'] + if ntype in ('expand', 'nestedExpand'): + # The title is usually the only summary of what the collapsed + # block holds, and collapsing is exactly what an author does + # with long logs and stack traces. + title = node.get('attrs', {}).get('title', '') + child_text = [] + for c in node.get('content', []): + child_text.extend(extract_text(c, depth)) + body = '\n'.join(child_text).strip('\n') + head = ['**' + title + '**'] if title else [] + return head + (body.split('\n') if body else []) + [''] + if ntype == 'decisionList': + for c in node.get('content', []): + lines.extend(extract_text(c, depth + 1)) + return lines + if ntype == 'decisionItem': + child_text = [] + for c in node.get('content', []): + child_text.extend(extract_text(c, depth)) + body = ' '.join('\n'.join(child_text).split()) + return [' ' * depth + '- [decision] ' + body] if body else [] if ntype == 'panel': # Jira panels carry their severity in the attrs, not the text, # so an info note and a warning read the same without this. @@ -360,7 +385,13 @@ if subtasks: # comments_data is the empty {} sentinel.) comments = comments_data.get('comments', []) if isinstance(comments_data, dict) else [] if comments: - print(f'{indent}Comments ({len(comments)}):') + # The endpoint caps a page at 100 comments. Saying so beats letting a + # reader believe a truncated thread is the whole discussion. + total = comments_data.get('total', len(comments)) + if isinstance(total, int) and total > len(comments): + print(f'{indent}Comments ({len(comments)} of {total}; rest not fetched):') + else: + print(f'{indent}Comments ({len(comments)}):') for c in comments: author = c.get('author', {}).get('displayName') or c.get('author', {}).get('emailAddress') or 'unknown' created = c.get('created', '')[:10] # YYYY-MM-DD @@ -422,6 +453,10 @@ try: status = issue['fields']['status']['name'] itype = issue['fields']['issuetype']['name'] print(f'{key} ({itype}) [{status}]: {summary}') + # maxResults caps this request; a parent with more children would + # otherwise show a short list indistinguishable from a complete one. + if data.get('isLast') is False or data.get('nextPageToken'): + print('(more children exist than were fetched)') except Exception: pass PYEOF_CHILD_KEYS_PY From 1570cb5cd98279bdb3bb1cd4226011430facd3b4 Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Mon, 7 Sep 2026 15:19:33 +0200 Subject: [PATCH 029/120] LCORE-3963: address review findings on escaping, nesting, exit status, paging Five problems raised in review, all reproduced against the extractor before being fixed. A failed relation during recursion was swallowed. The recursive call used '|| true' so that one unreachable relation would not abandon its siblings, but that also discarded the failure, and the run exited zero having printed 'Error fetching'. Recording it instead keeps both properties: the recursion continues and the exit status still reports the failure. This was a defect in the error-handling change earlier on this branch, which claimed the exit status was now correct. A code run containing a backtick produced a broken span, since the text was wrapped in single backticks regardless of its content. It is now fenced with one more backtick than its longest internal run, padded when the text begins or ends with one. A pipe inside a table cell opened a new column and shifted every cell after it; pipes and backslashes are escaped before the row is joined. A nested list was folded into its parent item's text, so a list rendered as '- parent - child' on a single line. The item now keeps its own text and its nested lines apart, and the enclosing list applies the marker only to the first of them, leaving the nested list's own indentation intact. The comment thread and both child searches read a single page, so a ticket with a long discussion or a parent with many children produced a quiet half-answer. The previous commit reported the truncation, which is better than hiding it but still loses the content. Both now follow the cursor to the end -- startAt for comments, nextPageToken for the search -- collecting pages under a scratch directory that a trap removes on exit, with a page limit so a malformed cursor cannot spin. The truncation notices stay as a safety net for that limit. Verified with a synthetic ADF document for the four rendering and status defects, each of which reproduced beforehand: a backtick inside a code run, a pipe inside a table cell, a nested list, and a relation failure leaving the status at zero. Paging was checked against synthetic multi-page responses, merging 250 comments across three pages and 107 issues across two in order and stopping at the right cursor. Against the live instance the four earlier invocations are byte identical, the epic listing still returns all sixteen children with no truncation notice, the scratch directory is removed, and the closed-port run still reports both tickets and exits 1. --- dev-tools/fetch-jira.sh | 192 ++++++++++++++++++++++++++++++++++------ 1 file changed, 165 insertions(+), 27 deletions(-) diff --git a/dev-tools/fetch-jira.sh b/dev-tools/fetch-jira.sh index 8182367ba..fee9fbd67 100755 --- a/dev-tools/fetch-jira.sh +++ b/dev-tools/fetch-jira.sh @@ -122,6 +122,10 @@ def clean_url(url): return url.split('#icft=')[0] if '#icft=' in url else url +# List kinds that may appear nested inside a listItem. +NESTED_LIST_TYPES = ('bulletList', 'orderedList', 'taskList') + + # ADF (Atlassian Document Format) → markdown-ish text extractor. # Hoisted to top-level so both description and comments can use it. def extract_text(node, depth=0): @@ -136,7 +140,18 @@ def extract_text(node, depth=0): if m.get('type') == 'strong': text = f'**{text}**' elif m.get('type') == 'code': - text = f'`{text}`' + # A code run may itself contain backticks, which would + # end the span early. Fence it with one more backtick + # than its longest internal run, padding when the text + # starts or ends with one. + longest = 0 + run = 0 + for char in text: + run = run + 1 if char == '`' else 0 + longest = max(longest, run) + fence = '`' * (longest + 1) + pad = ' ' if text.startswith('`') or text.endswith('`') else '' + text = fence + pad + text + pad + fence elif m.get('type') == 'em': text = f'_{text}_' elif m.get('type') == 'strike': @@ -215,15 +230,22 @@ def extract_text(node, depth=0): return [] return joined.split('\n') + [''] if ntype == 'listItem': - child_text = [] + own_text = [] + nested = [] for c in node.get('content', []): - child_text.extend(extract_text(c, depth)) + if isinstance(c, dict) and c.get('type') in NESTED_LIST_TYPES: + # A nested list has already indented its own lines; folding + # it into the parent's text would flatten the whole tree + # onto one line. + nested.extend(extract_text(c, depth)) + else: + own_text.extend(extract_text(c, depth)) # Join on a newline before collapsing so an item holding two # paragraphs keeps a space between them instead of running the # last word of one into the first word of the next. The marker # is added by the enclosing list, which is the only place that # knows whether the item needs a bullet or a number. - return [' '.join('\n'.join(child_text).split())] + return [' '.join('\n'.join(own_text).split())] + nested if ntype in ('bulletList', 'orderedList'): ordered = ntype == 'orderedList' first = node.get('attrs', {}).get('order', 1) if ordered else 1 @@ -232,11 +254,14 @@ def extract_text(node, depth=0): except (ValueError, TypeError): first = 1 for number, c in enumerate(node.get('content', []), start=first): - for item in extract_text(c, depth + 1): - if not item: - continue - marker = f'{number}. ' if ordered else '- ' - lines.append(' ' * (depth + 1) + marker + item) + item_lines = [item for item in extract_text(c, depth + 1) if item] + if not item_lines: + continue + marker = f'{number}. ' if ordered else '- ' + lines.append(' ' * (depth + 1) + marker + item_lines[0]) + # Anything after the first line is a nested list that brought + # its own marker and indentation. + lines.extend(item_lines[1:]) return lines if ntype == 'taskList': for c in node.get('content', []): @@ -323,7 +348,11 @@ def extract_text(node, depth=0): # middle of the row and break the whole table. Join on a # newline first so the paragraph boundary survives as the # space that separates them. - cells.append(' '.join('\n'.join(cell_text).split())) + cell = ' '.join('\n'.join(cell_text).split()) + # An unescaped pipe in the text would open a new column + # and shift every cell after it. + cell = cell.replace('\\', '\\\\').replace('|', '\\|') + cells.append(cell) if not cells: continue rows.append(' | '.join(cells)) @@ -413,6 +442,65 @@ if comments: PYEOF_PRINT_TICKET_PY ) +# Prints the next startAt offset for a comment page, or nothing when the +# thread is exhausted. +COMMENT_NEXT_PY=$(cat <<'PYEOF_COMMENT_NEXT_PY' +import json, sys +try: + page = json.load(open(sys.argv[1])) + start = int(page.get('startAt', 0)) + got = len(page.get('comments', [])) + total = int(page.get('total', 0)) + if got and start + got < total: + print(start + got) +except Exception: + pass +PYEOF_COMMENT_NEXT_PY +) + +# Merges numbered comment pages back into one response-shaped document. +MERGE_COMMENTS_PY=$(cat <<'PYEOF_MERGE_COMMENTS_PY' +import json, os, sys +directory = sys.argv[1] +names = sorted(os.listdir(directory), key=lambda f: int(f.split('.')[0])) +comments = [] +total = 0 +for name in names: + page = json.load(open(os.path.join(directory, name))) + comments.extend(page.get('comments', [])) + total = max(total, int(page.get('total', 0))) +print(json.dumps({'comments': comments, 'total': max(total, len(comments))})) +PYEOF_MERGE_COMMENTS_PY +) + +# Prints the search cursor for the next page, or nothing when it is the last. +SEARCH_NEXT_PY=$(cat <<'PYEOF_SEARCH_NEXT_PY' +import json, sys +try: + page = json.load(open(sys.argv[1])) + token = page.get('nextPageToken') + if token and page.get('isLast') is not True: + print(token) +except Exception: + pass +PYEOF_SEARCH_NEXT_PY +) + +# Merges numbered search pages into one issues list. +MERGE_ISSUES_PY=$(cat <<'PYEOF_MERGE_ISSUES_PY' +import json, os, sys +directory = sys.argv[1] +names = sorted(os.listdir(directory), key=lambda f: int(f.split('.')[0])) +issues = [] +last = True +for name in names: + page = json.load(open(os.path.join(directory, name))) + issues.extend(page.get('issues', [])) + last = page.get('isLast') is not False and not page.get('nextPageToken') +print(json.dumps({'issues': issues, 'isLast': last})) +PYEOF_MERGE_ISSUES_PY +) + RELATED_KEYS_PY=$(cat <<'PYEOF_RELATED_KEYS_PY' import json, sys try: @@ -462,6 +550,65 @@ except Exception: PYEOF_CHILD_KEYS_PY ) +# Jira serves list endpoints one page at a time. Pages are collected as files +# under a scratch directory and merged, so a long comment thread or a parent +# with many children is returned whole rather than silently cut off at the +# first page. PAGE_LIMIT is a stop so a malformed cursor cannot spin forever. +PAGE_LIMIT=50 +PAGE_DIR=$(mktemp -d) +trap 'rm -rf "$PAGE_DIR"' EXIT + +fetch_all_comments() { + local key="$1" + local dir="$PAGE_DIR/comments" + rm -rf "$dir" + mkdir -p "$dir" + local start=0 + local page_no=0 + while [ "$page_no" -lt "$PAGE_LIMIT" ]; do + curl -sS --connect-timeout 10 --max-time 30 \ + -u "$JIRA_EMAIL:$JIRA_TOKEN" \ + "$JIRA_INSTANCE/rest/api/3/issue/$key/comment?startAt=$start&maxResults=100" \ + -o "$dir/$page_no.json" 2>/dev/null || return 1 + start=$(python3 -c "$COMMENT_NEXT_PY" "$dir/$page_no.json" 2>/dev/null) || return 1 + page_no=$((page_no + 1)) + if [ -z "$start" ]; then + break + fi + done + python3 -c "$MERGE_COMMENTS_PY" "$dir" 2>/dev/null || return 1 +} + +fetch_all_children() { + local parent="$1" + local fields="$2" + local dir="$PAGE_DIR/children" + rm -rf "$dir" + mkdir -p "$dir" + local token='' + local page_no=0 + local url + while [ "$page_no" -lt "$PAGE_LIMIT" ]; do + url="$JIRA_INSTANCE/rest/api/3/search/jql?jql=parent%3D${parent}&fields=${fields}&maxResults=100" + if [ -n "$token" ]; then + url="$url&nextPageToken=$token" + fi + curl -sS --connect-timeout 10 --max-time 30 \ + -u "$JIRA_EMAIL:$JIRA_TOKEN" "$url" -o "$dir/$page_no.json" 2>/dev/null || return 1 + token=$(python3 -c "$SEARCH_NEXT_PY" "$dir/$page_no.json" 2>/dev/null) || return 1 + page_no=$((page_no + 1)) + if [ -z "$token" ]; then + break + fi + done + python3 -c "$MERGE_ISSUES_PY" "$dir" 2>/dev/null || return 1 +} + +# Every requested ticket is attempted even when an earlier one fails, so one +# unreachable key does not hide the rest of the output; the script still exits +# non-zero if any of them failed, including a failure deep in the recursion. +EXIT_STATUS=0 + fetch_ticket() { local key="$1" local indent="${2:-}" @@ -486,9 +633,7 @@ fetch_ticket() { # JSON object signals "no comments fetched" to the Python printer. local comments_data='{}' if [ "$FETCH_COMMENTS" -eq 1 ]; then - comments_data=$(curl -sS --connect-timeout 10 --max-time 30 \ - -u "$JIRA_EMAIL:$JIRA_TOKEN" \ - "$JIRA_INSTANCE/rest/api/3/issue/$key/comment" 2>/dev/null) || comments_data='{}' + comments_data=$(fetch_all_comments "$key") || comments_data='{}' fi if echo "$data" | python3 -c "import sys,json; json.load(sys.stdin)['key']" >/dev/null 2>&1; then @@ -507,27 +652,22 @@ fetch_ticket() { # Also fetch JQL parent= children local jql_kids - jql_kids=$(curl -sS --connect-timeout 10 --max-time 30 \ - -u "$JIRA_EMAIL:$JIRA_TOKEN" \ - "$JIRA_INSTANCE/rest/api/3/search/jql?jql=parent%3D${key}&fields=key&maxResults=20" 2>/dev/null | \ + jql_kids=$(fetch_all_children "$key" "key" 2>/dev/null | \ python3 -c "$JQL_KIDS_PY" 2>/dev/null | tr '\n' ' ') || jql_kids='' local rk for rk in $related_keys $jql_kids; do [ -z "$rk" ] && continue echo - # A relation we cannot fetch is reported by fetch_ticket and - # then tolerated: it must not abandon the rest of the recursion. - fetch_ticket "$rk" "${indent} " $((depth - 1)) || true + # A relation we cannot fetch must not abandon the rest of the + # recursion, but it is still a failure: record it rather than + # swallowing it, or the run reports success having printed an + # error. + fetch_ticket "$rk" "${indent} " $((depth - 1)) || EXIT_STATUS=1 done fi } -# Every requested ticket is attempted even when an earlier one fails, so -# one unreachable key does not hide the rest of the output; the script -# still exits non-zero if any of them failed. -EXIT_STATUS=0 - # Fetch main ticket (with depth recursion if requested) fetch_ticket "$TICKET" "" "$LINKED_DEPTH" || EXIT_STATUS=1 @@ -536,9 +676,7 @@ fetch_ticket "$TICKET" "" "$LINKED_DEPTH" || EXIT_STATUS=1 # fetching each one). At depth > 0, the recursive fetch_ticket already # pulled them in, so skip this listing to avoid duplication. if [ "$LINKED_DEPTH" -eq 0 ]; then - CHILD_KEYS=$(curl -sS --connect-timeout 10 --max-time 30 \ - -u "$JIRA_EMAIL:$JIRA_TOKEN" \ - "$JIRA_INSTANCE/rest/api/3/search/jql?jql=parent%3D${TICKET}&fields=key,summary,status,issuetype&maxResults=20" 2>/dev/null | \ + CHILD_KEYS=$(fetch_all_children "$TICKET" "key,summary,status,issuetype" 2>/dev/null | \ python3 -c "$CHILD_KEYS_PY" 2>/dev/null) || CHILD_KEYS='' if [ -n "$CHILD_KEYS" ]; then From 33b6abe231f3c798b7add24730abb4cca8f1689e Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Tue, 8 Sep 2026 09:11:35 +0200 Subject: [PATCH 030/120] LCORE-3299: Fixed issue in vulnerability report --- scripts/vulnerability_report.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/vulnerability_report.py b/scripts/vulnerability_report.py index c6503ad12..28364695b 100644 --- a/scripts/vulnerability_report.py +++ b/scripts/vulnerability_report.py @@ -503,13 +503,13 @@ def generate_vuln_for_days_graph( svg_output (bool): If true, save the graph as SVG. png_output (bool): If true, save the graph as PNG. """ - fig, ax = plt.subplots() - D = stat["days"]["days"] - ax.hist(D, bins=30, edgecolor="black") + _, ax = plt.subplots() + data = stat["days"]["days"] + ax.hist(data, bins=30, edgecolor="black") ax.set_ylim(top=100) ax.set_xlabel("Days") ax.set_title("Fixed in day(s)") - save_graph(fig, prefix, "days", svg_output, png_output) + save_graph(prefix, "days", svg_output, png_output) def generate_vulnerable_packages_graph( From 69feb6a29460ba6da92b3ce48d55eb5c8cfc7930 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Tue, 8 Sep 2026 09:16:59 +0200 Subject: [PATCH 031/120] LCORE-3987: Do not assert in production code --- tests/e2e/features/steps/common_http.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/features/steps/common_http.py b/tests/e2e/features/steps/common_http.py index 5ce2bd52c..7efffef5d 100644 --- a/tests/e2e/features/steps/common_http.py +++ b/tests/e2e/features/steps/common_http.py @@ -35,7 +35,7 @@ def check_status_code(context: Context, status: int) -> None: error_body = context.response.json() except JSONDecodeError: error_body = context.response.text - assert False, ( + raise AssertionError( f"Status code is {context.response.status_code}, expected {status}. " f"Response: {error_body}" ) @@ -52,7 +52,7 @@ def check_status_code_one_of(context: Context, first: int, second: int) -> None: error_body = context.response.json() except JSONDecodeError: error_body = context.response.text - assert False, ( + raise AssertionError( f"Status code is {actual}, expected one of {sorted(allowed)}. " f"Response: {error_body}" ) From 81e9592df01b1a601fa2dffe71208e12b2a91da8 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Tue, 8 Sep 2026 09:19:38 +0200 Subject: [PATCH 032/120] LCORE-3989: Do not assert in production code --- tests/e2e/utils/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/utils/utils.py b/tests/e2e/utils/utils.py index 1025114fe..378dbde12 100644 --- a/tests/e2e/utils/utils.py +++ b/tests/e2e/utils/utils.py @@ -180,10 +180,10 @@ def validate_json(message: Any, schema: Any) -> None: ) except jsonschema.ValidationError as e: - assert False, "The message doesn't fit the expected schema:" + str(e) + raise AssertionError("The message doesn't fit the expected schema:" + str(e)) except jsonschema.SchemaError as e: - assert False, "The provided schema is faulty:" + str(e) + raise AssertionError("The provided schema is faulty:" + str(e)) def wait_for_container_health( From 4ffae4060873ba80dbd49a116dbdf23a63a5dc1c Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Tue, 8 Sep 2026 09:23:48 +0200 Subject: [PATCH 033/120] LCORE-3993: Hexagonal architecture --- docs/devel_doc/hexagonal_architecture.svg | 1122 +++++++++++++++++++++ 1 file changed, 1122 insertions(+) create mode 100644 docs/devel_doc/hexagonal_architecture.svg diff --git a/docs/devel_doc/hexagonal_architecture.svg b/docs/devel_doc/hexagonal_architecture.svg new file mode 100644 index 000000000..13ed68baf --- /dev/null +++ b/docs/devel_doc/hexagonal_architecture.svg @@ -0,0 +1,1122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Service + Domain + + + Port + + Port + + Port + + Port + + Port + + Port + + Adapter + + Adapter + + Port + + Adapter + + Adapter + + Adapter + + Adapter + + Adapter + + Request flow + + + REST API + Database + LLMs + Telemetry + Metrics + OKP MCP + BYOK + Agents + + Port + + Adapter + + Primary actors + Secondary actors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 0c1d31c07977bf3eec9eec842826cd3e44443ea7 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Tue, 8 Sep 2026 13:51:13 +0200 Subject: [PATCH 034/120] LCORE-3580: Updated dependencies --- uv.lock | 154 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/uv.lock b/uv.lock index 6e69f2411..63da73e72 100644 --- a/uv.lock +++ b/uv.lock @@ -218,46 +218,46 @@ wheels = [ [[package]] name = "ast-serialize" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fd/c0/5bb6885a9608d86ee5712c0d88bc405d3a49f3e44231576e130ea2f53d34/ast_serialize-0.9.0.tar.gz", hash = "sha256:79fe8be1c934aa572940d1811d8dbe4d1b6f22291e3f16755c9b062e9ac92fb7", size = 951293, upload-time = "2026-09-02T15:50:45.078Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/f9/a4af1bf8b35927814c09d90c3965dbfaa75c489ba34372bffafbc2209f40/ast_serialize-0.9.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:383f56e3ae925f154632458f01b4bfcde3dd382f3ec04f5c7f6d72f76524ff48", size = 1226344, upload-time = "2026-09-02T15:49:49.79Z" }, - { url = "https://files.pythonhosted.org/packages/1d/6d/d3a95823a803c21f5c9df595a0bb93aada22e7aa22bf875fe00d89422d7f/ast_serialize-0.9.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:b9ef3d4173907bd19aa8f1683be9f06e7862e6cdf2ca6bca3633305c0df32063", size = 1207384, upload-time = "2026-09-02T15:49:51.22Z" }, - { url = "https://files.pythonhosted.org/packages/f0/97/6e7f46c8455b738609c29d1b7655307a168c4b40ce4c7a2c678c8ed9cf2e/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9482672ca8ec09f85cd050a053fb88c30c882c8e20ce7a140d8defe19c0ef2eb", size = 1273139, upload-time = "2026-09-02T15:49:52.679Z" }, - { url = "https://files.pythonhosted.org/packages/f0/6e/25b70733f061766865cb04d913dc5332037c595796b871d52ab5b569abb8/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6a568d1d489f0669a31aed90ca4845aa7f08e1b8cd5d05e1905dbdc3ae9b2b0", size = 1278242, upload-time = "2026-09-02T15:49:54.236Z" }, - { url = "https://files.pythonhosted.org/packages/9a/f0/b7820399d9c5a0b7f07c239b6da93d2e21a1b3785137fa00e16528e414b3/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a4dd005d4095a13eb312dc712c943c7730f262b000a87df963925328a38ffdb", size = 1541009, upload-time = "2026-09-02T15:49:56.149Z" }, - { url = "https://files.pythonhosted.org/packages/08/56/5146f1d2a77516e697f6f42825df79137e43560675cb4605c467775f8b4a/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:207ac73afa1f4654840593853c130eac2591dd942434176eea33f730afb3359b", size = 1290898, upload-time = "2026-09-02T15:49:57.502Z" }, - { url = "https://files.pythonhosted.org/packages/69/c4/87cd16228796d703de795a369b90b0f57f0f017f90f55c4c5876e3513a03/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ae01129e2cc5d57a3c434d8a990019de039350310a2e1dd3c9f61311964cf25", size = 1291742, upload-time = "2026-09-02T15:49:58.982Z" }, - { url = "https://files.pythonhosted.org/packages/cc/eb/13465c297268c5170b2bb746d75f37a8fad44a94a89b593071affc1071d0/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:4c522377f383670abfe21c94edc3032cb3bd34d8fcacd280fa9556907d4edd4b", size = 1300180, upload-time = "2026-09-02T15:50:00.454Z" }, - { url = "https://files.pythonhosted.org/packages/0a/a7/10b84c4274b2507b0ed9cc1654058ad64bcedb6ac574753d6a461bc6e204/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4cd886f6e900f5e13f758cb8ef359652e690e4f3f9c257a7269e095940534167", size = 1345857, upload-time = "2026-09-02T15:50:01.875Z" }, - { url = "https://files.pythonhosted.org/packages/c7/dc/2702182c9773a15de9aabfaf66da7cb87548a56a6bac24f0c9176a4a13c3/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:f3f0f3359cf0f22bf096b07021ffe6bf0ec88ac8a2cf7ce5f4701af973112faa", size = 1448544, upload-time = "2026-09-02T15:50:03.496Z" }, - { url = "https://files.pythonhosted.org/packages/59/b5/eeef2124c9563b9861707ef4db91f153f3bb37b3e0cca9543bb88a4e9e53/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:6c428444656cffbd32c1e76626c6eec5237b58c8fcb0b5d3df75941cd50c4f3c", size = 1551572, upload-time = "2026-09-02T15:50:04.982Z" }, - { url = "https://files.pythonhosted.org/packages/cc/8c/81d18349f1dffdcfeb80671bd737e342c86348e183692d9d0d8f573d1385/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:54f0babed5e2a4eb86a0716ac612aff33f933e7572e5bc067adcdbe672a26321", size = 1548118, upload-time = "2026-09-02T15:50:06.522Z" }, - { url = "https://files.pythonhosted.org/packages/d4/1f/339131b60d1b0df13d9f3470cfac70f858b5649188ea01b2e7b39caeb720/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:1b779fdaee34d19900a5ba5fd6bd4cefe225650081f9de28de256eacee5113c2", size = 1674707, upload-time = "2026-09-02T15:50:07.919Z" }, - { url = "https://files.pythonhosted.org/packages/18/0a/ca77596fa229d88f96eca180d45dbe8efa11306f8b2b4f4ee301b3fe465f/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:4db7f524eaa857fbe650cac33b9cedb5ccda14393d40f640b75dfb06aa13c98c", size = 1473618, upload-time = "2026-09-02T15:50:09.312Z" }, - { url = "https://files.pythonhosted.org/packages/88/5c/6aebeb54dd226b480014ff4488e150aa23b1de3204e2bf3f87de27e6542a/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:d2a37795a90809da6094825e7063118b4cf723b8701b134973b4566ed8b9ea09", size = 1492025, upload-time = "2026-09-02T15:50:10.755Z" }, - { url = "https://files.pythonhosted.org/packages/75/e6/c355d470a230f778311c28b80f6d934a497d094139f07230433eea18651b/ast_serialize-0.9.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:4411d1cba9eeecb301365343a7e96813b4a44fcdb20181557867ff7e751804cf", size = 1113010, upload-time = "2026-09-02T15:50:12.337Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0d/66609ace58564727b68731293cc986c2ea1d5e6ef40e96571e7fb515f0af/ast_serialize-0.9.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:b5c3724faf780e25def89c369eb6340a15dff06ac348160c61781e2373a4cd10", size = 1146404, upload-time = "2026-09-02T15:50:13.761Z" }, - { url = "https://files.pythonhosted.org/packages/b1/fd/da28e1c85f05fb9976f247d2a3aefce68866cb2939abcbdbddd9a5e3b835/ast_serialize-0.9.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:1de0933a4c1d104d77d6e75f053f5e628b54cf8f9fea809b8250cb04cda07bd3", size = 1118328, upload-time = "2026-09-02T15:50:15.214Z" }, - { url = "https://files.pythonhosted.org/packages/92/e4/175b0a64d6c96bc1b96598c6474ce8d1ef34e0b774bcf7183f4ce696fb10/ast_serialize-0.9.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dac690f99538d9df0d23ce0299e946add2744b007a36b480a292fe361c82553d", size = 1232635, upload-time = "2026-09-02T15:50:18.133Z" }, - { url = "https://files.pythonhosted.org/packages/28/0c/d51d8463aca43aaa833fdf1f25134d6cc1b483764896decca61306ad1f6e/ast_serialize-0.9.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:2223ead73b5a5399d39610cf9c4164ad0b2bf2025226626b87ae15226d93d3f7", size = 1219313, upload-time = "2026-09-02T15:50:19.497Z" }, - { url = "https://files.pythonhosted.org/packages/ef/19/c88bdc64f86095a9d6ab325ae422b2a5e1395cd63cd8aa539003d4d4ae1d/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b7c5f5838408fb000d76abd14e886836412b7ec7eccd028dbb5ed5819780008", size = 1279981, upload-time = "2026-09-02T15:50:20.811Z" }, - { url = "https://files.pythonhosted.org/packages/86/58/a492075826df1753896dc8e8f6ababae4016d8883b670ee3a1c34788b154/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e05701fde79affa1cc53e391867f9da3eb03fa8501f87354292796b0f8398fd", size = 1286319, upload-time = "2026-09-02T15:50:22.203Z" }, - { url = "https://files.pythonhosted.org/packages/ae/79/3f6754eaa42fd2a6c36aac066890870cd44cbe0e25f75a67b1b99a2f4d82/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d013c36eb2f2ac0cb7d4d0e79918a92ab00fbce8f1542fe47f34a46e06168f82", size = 1551547, upload-time = "2026-09-02T15:50:23.528Z" }, - { url = "https://files.pythonhosted.org/packages/b1/05/8cfb7caadfaf28febaa6b61d31d778262f87f9366eda4dd9bd07ac940b75/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19cde5c2110f7b90ab1210a599178524f6c9f34862b20ba2b9aa7832c67bb35d", size = 1302468, upload-time = "2026-09-02T15:50:24.99Z" }, - { url = "https://files.pythonhosted.org/packages/aa/e2/750a0b136bb02ff8e4a17d65a3a78cd478ee50724704df8215797a226ba3/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1514f4a39704e2e815f9fc675fc13f19f694f212086b520110840782cf3c5295", size = 1300563, upload-time = "2026-09-02T15:50:26.354Z" }, - { url = "https://files.pythonhosted.org/packages/ab/17/4c0aa852ff1e4f2d6723e8ce827136c1e1febf2845d7941ccc45426778de/ast_serialize-0.9.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:30651ccdec6d23c49ee4711b1a1096d8dbd3be38eecf2f09fdd98a608ce7ac24", size = 1308999, upload-time = "2026-09-02T15:50:27.901Z" }, - { url = "https://files.pythonhosted.org/packages/4d/1b/6e73d0a29aedb0db30cc68f2557acaac06cd24c9783ccb90f84f89e4ce87/ast_serialize-0.9.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d9a46caf5e3f2cd266e8638b2f4ea8bf54cf376f015f8418397b4633fdb38e9b", size = 1358191, upload-time = "2026-09-02T15:50:29.237Z" }, - { url = "https://files.pythonhosted.org/packages/dc/38/2cf5d552de99e0e9804a16fea73e54d0a7382498adddf57c0f6dc09cbc70/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:aed6e413c6c22a23c33c47a01dd2adce01d7a7ed408748e896903f47d0a1aa47", size = 1458944, upload-time = "2026-09-02T15:50:30.77Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0b/5ef87adf955b6a027f616eb7b55f55a154c35ba600e9dd2d06ad2d30e5c2/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:871fb7c5b049897ee137b67efad7fe4545ad270f7eccd970da877833f8e63aa7", size = 1563421, upload-time = "2026-09-02T15:50:32.188Z" }, - { url = "https://files.pythonhosted.org/packages/81/dd/9ced05a17feeb0f83e84010d80f5a1b7b7aa19e75f0376f4d3780803654c/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:bb378efb5537b43f38660e2e6d6e138a40885cf191d43443bb3ff7ff47e9cd9b", size = 1558536, upload-time = "2026-09-02T15:50:33.861Z" }, - { url = "https://files.pythonhosted.org/packages/5f/8b/8ad486e44fc7081a2471055befc433dddc2e51c3a88dff141b3026f64602/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:b373beff65b01fcffca5aaad3269ae629f3a998b09efdd3635e48039008a5dec", size = 1682749, upload-time = "2026-09-02T15:50:35.257Z" }, - { url = "https://files.pythonhosted.org/packages/dd/4c/7c282aba9cfb0b92d79fac45c04e4557d9a7f08d872e5a43577a50867e30/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:25c8d517c45cf2b1820fc2af6ac593783654818f79d05646d25d624360678a4e", size = 1482441, upload-time = "2026-09-02T15:50:37.319Z" }, - { url = "https://files.pythonhosted.org/packages/a4/3a/e45914e8cad81b660915f3784d255460a6384183b76bfc2089fdd79ec7df/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1675dc46578298ae00936164a160997801a6ca2385913150d8d16df634296cf3", size = 1499042, upload-time = "2026-09-02T15:50:38.76Z" }, - { url = "https://files.pythonhosted.org/packages/99/09/6988921dec19c810beef53539fec2e90ae551cd93853b3303a99fe45f772/ast_serialize-0.9.0-cp39-abi3-win32.whl", hash = "sha256:20fce3885eeff05a3d6afefa845c8168016e3ea1f6fc9cdc84c8db28b863a550", size = 1116391, upload-time = "2026-09-02T15:50:40.229Z" }, - { url = "https://files.pythonhosted.org/packages/fd/eb/839598a22a1f9af56d39e188451cad93dbcb0ce6539a45ac18fb8bf123fa/ast_serialize-0.9.0-cp39-abi3-win_amd64.whl", hash = "sha256:161914666a21d48b681982146ac0fa4086ef099d91c637cf595387f5f06aa099", size = 1156055, upload-time = "2026-09-02T15:50:42.05Z" }, - { url = "https://files.pythonhosted.org/packages/0d/45/c7cd8d36d3b506bbd02db5066fae3340284781168f0d08dac25deef5f69d/ast_serialize-0.9.0-cp39-abi3-win_arm64.whl", hash = "sha256:74473258a5c55855d5306c864a5c799fbff03a0f0ea1197346b2b5cc5b4ea48a", size = 1128237, upload-time = "2026-09-02T15:50:43.496Z" }, +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/6d/c6ab91f72f4862d63e446e10248d8f52e8b4c4b7579bb937ddf942a5961e/ast_serialize-0.10.0.tar.gz", hash = "sha256:f47a26cc7d2605fb645b6e7f6c21cf4fb8d7833d00ea8b48e26f057b14eccd01", size = 952608, upload-time = "2026-09-07T10:22:47.595Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/bd/cf2deab2ceb79f4476a30479a2756bb64b63810b7f10bfbd3ce1503d4059/ast_serialize-0.10.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:cbcc3542259ae08153e634d50dc220723415fb24510856523052b5eeb10b4950", size = 1229414, upload-time = "2026-09-07T10:21:49.554Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cb/ec8e84d3ab8073e8536823f7a3cba46e4992d9c8d904481e18509eeea3ff/ast_serialize-0.10.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:2251086be53a375d256faefaedffe5ccd99187be614788f8279b41dae1c5fdee", size = 1210297, upload-time = "2026-09-07T10:21:50.996Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e7/6a1d216160da29566a6125c34e90092a5f9d1444e24e955741113abdf033/ast_serialize-0.10.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1358ef5d98f9dc96468d171317d78d746f9ef8ca45577298512a737ba9d6514", size = 1276064, upload-time = "2026-09-07T10:21:52.452Z" }, + { url = "https://files.pythonhosted.org/packages/96/87/1f1052a0dd00dfe15c129a7073f7b1b5bb632b5507baff27190dd255e0a9/ast_serialize-0.10.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:59ef8acc0ba4315024e5000e5fa8ea656a02e375ebb5ce020f3582d308d6153d", size = 1280982, upload-time = "2026-09-07T10:21:53.992Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8f/9f4578b668b60436249dfa1bc6612283d3cbb665bc6cb2c1e23934d708d8/ast_serialize-0.10.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c9715c4379b33740c94819e55afe59e28d6d3f30f69b903b4fcb2ecb5335531", size = 1552458, upload-time = "2026-09-07T10:21:55.485Z" }, + { url = "https://files.pythonhosted.org/packages/e6/cc/395d96c55e5d91a87fcfa5f15c7d11834cd150d491ce4849b2290334b8e9/ast_serialize-0.10.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26e6dc5d1f7aa642d9e94b98e4d3e1c81dc6b595f01619658f7e9268379826c0", size = 1298414, upload-time = "2026-09-07T10:21:57.01Z" }, + { url = "https://files.pythonhosted.org/packages/7a/8d/473c8fb551af1de55abcc36d591ac18d4175ba7fc15f41d62ad9b30d7385/ast_serialize-0.10.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760dce78c6c4d4df30d3df9b80be1c521e0ee1d275ae54906e9a95b150692567", size = 1296613, upload-time = "2026-09-07T10:21:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/6f/57/31044aa6fa739634ada7fff321db08a813157663b18629851820fca455a6/ast_serialize-0.10.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:40a7f487e2f5523518270600e0b7f2c61beac3106da142d911c3386ed7abbcfa", size = 1304321, upload-time = "2026-09-07T10:22:00.12Z" }, + { url = "https://files.pythonhosted.org/packages/10/87/c5d7c0c627b991f46fc308a41c0e92b792322440f84e43739b11fb4a1a64/ast_serialize-0.10.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1c252b746da0ad1883b82502eeff859221eef1d66d3f95dcb2373760a34d440", size = 1350352, upload-time = "2026-09-07T10:22:01.605Z" }, + { url = "https://files.pythonhosted.org/packages/62/ec/d80abe681b7d96b4ea88cf69d559d55a41b1a36c907b579d774caff2527a/ast_serialize-0.10.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:719c7a121cc3022d78b7f4f7a9fd159586e521531c0e495a245375d2c4695d4e", size = 1454522, upload-time = "2026-09-07T10:22:03.149Z" }, + { url = "https://files.pythonhosted.org/packages/8d/7a/d4cc3aa236b9fdce16a394bfd7c12072d9ac30e83f9319aaa14305cbaab9/ast_serialize-0.10.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:d7621157ac99ae00957196e7094fe279e7547d8b505046a9d5509c4fc740e9fc", size = 1555150, upload-time = "2026-09-07T10:22:04.722Z" }, + { url = "https://files.pythonhosted.org/packages/d2/a6/710d1aef678650c75edcb4cdab79998ec63aae54710a438b9fd66c3a616e/ast_serialize-0.10.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:82087a71fe39925ab0068ef2f25444a1c87d1e5f1901f32dcf82f6eddddeac6a", size = 1551843, upload-time = "2026-09-07T10:22:06.182Z" }, + { url = "https://files.pythonhosted.org/packages/e9/23/2a9c35721a82a506bae980dbdfeaa7b296bf79d818c731b3aaa0ee53c5a7/ast_serialize-0.10.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:8aac958cf0c0b2595a0487c7b5d2ecac88fa5c0221b83b1420ac4bf472f84cb7", size = 1686064, upload-time = "2026-09-07T10:22:07.712Z" }, + { url = "https://files.pythonhosted.org/packages/24/36/f3fd4b6f2c1e4d5eca149e4bfdaa6d93fe77851b21562d14f22a452c71ee/ast_serialize-0.10.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:a53a4d528171b8b709b4e7d89654532189bff53947f24ac4db9c465bb10e7cd2", size = 1478662, upload-time = "2026-09-07T10:22:09.218Z" }, + { url = "https://files.pythonhosted.org/packages/de/65/5135ac9c7e305bd3e6234ef0fbf50bbac2ac00a35076bc167936c6d6d166/ast_serialize-0.10.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:93ae740c6d6f5641573122a7dcffa4baa7ef9144109c54584baad5bf827ad388", size = 1495057, upload-time = "2026-09-07T10:22:10.742Z" }, + { url = "https://files.pythonhosted.org/packages/5a/dd/7dd0e80551dc66fc56197b5439d76fb6173ccf9975df6bf1399e2231069c/ast_serialize-0.10.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:4d10d8fb4b82d5bae455ce9f606920d8dcd563b812b43dcd8d6f9b11089cfb23", size = 1114912, upload-time = "2026-09-07T10:22:12.675Z" }, + { url = "https://files.pythonhosted.org/packages/72/83/a84661b89065fe4986d183abdf0bce5f1a24bea26bbe74054345758357c1/ast_serialize-0.10.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:5917460c0f8322755872db1792788fcaa3835d1fb6a3f129c65ace700c7e4663", size = 1152503, upload-time = "2026-09-07T10:22:14.159Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c0/95bc047bc830f3223d42111ca2980a4328cd09d597773f04f2c689c3330e/ast_serialize-0.10.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:8efb53cf1c439516a563664e014ba9b6b4fd86b4869c180954edce3954a51870", size = 1122527, upload-time = "2026-09-07T10:22:15.656Z" }, + { url = "https://files.pythonhosted.org/packages/70/85/4606f3398e6776c74d44f8387855775c3cf31614b664cdb0e147df752933/ast_serialize-0.10.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:382cb721e2624dd1c2f4f7afd2a9b500930b057d2e24e1078cf6999c5fad1b31", size = 1236237, upload-time = "2026-09-07T10:22:18.767Z" }, + { url = "https://files.pythonhosted.org/packages/09/7b/520b33339f3d5ef9407fb17a00dac0849318158fa0c9853cfe8cc6b7a48d/ast_serialize-0.10.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:56a28b7567ba17f602a17d5603133a1292d307e957b3b5c3f66538e87548aed3", size = 1223110, upload-time = "2026-09-07T10:22:20.243Z" }, + { url = "https://files.pythonhosted.org/packages/a0/af/3c30941ed368ddb54e6ec0edfdd98dc7fdc679c6d4f478f8545c0b8f080a/ast_serialize-0.10.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8919e79a4525cdac63d51cd117c57f324936c902c7ecd0fd09d02aa88323b74d", size = 1285870, upload-time = "2026-09-07T10:22:22.019Z" }, + { url = "https://files.pythonhosted.org/packages/56/81/f0c0bf569388d319aab22c8a7f43747450e26c5ae6a6d9521766157f70f4/ast_serialize-0.10.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1109077d53b377d022d799bc4b171c32c3a711a716248a918d3e07875b5513cd", size = 1290793, upload-time = "2026-09-07T10:22:23.562Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a1/515a76479ecfc77abe88d7c9954bc3a04f07b30fa33ec836eea80be07fd4/ast_serialize-0.10.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d17c50e1f6e8d950d79c66a18b863dee16cea3106f597203883131dd9c6ca4f", size = 1561250, upload-time = "2026-09-07T10:22:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ba/702af61eb7a6803bf4cd30c09041ab4212e63d553ed9dc2793c95ab340b4/ast_serialize-0.10.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf2c6cc5a62839869317523e0f74737db0d1759d72fb62cffe57a40e040d6a55", size = 1305696, upload-time = "2026-09-07T10:22:26.612Z" }, + { url = "https://files.pythonhosted.org/packages/31/ce/e6e0a311c16f84f13528f1cb32dec1ce68102711070c10c0c688a6c3cdf5/ast_serialize-0.10.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb1b20121a62da5937b0c11c8f31667be0102342bf55459bb44a10d8549f8f3f", size = 1302712, upload-time = "2026-09-07T10:22:28.088Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a2/7fd715228a35fec1c587c8156eaabf37b6df98fea48c90eec8f1af362e20/ast_serialize-0.10.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:6e45f7d7a663c28ed44d52b4069e32335edb7a0653a908a43bef2801d8d7c344", size = 1313977, upload-time = "2026-09-07T10:22:29.642Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ff/e1c6a0aefdc80695c2dda0c558d58f90f55cb7bb791545865cd7d4ec39d7/ast_serialize-0.10.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c75ed7d94660e3ed4fd3c35672ab16c248c0be51d6b845de3edebbb6cf4526c", size = 1360341, upload-time = "2026-09-07T10:22:31.289Z" }, + { url = "https://files.pythonhosted.org/packages/08/ef/ab341936f65b663e4909dd5d8772d7c13dd7a39616cf18fd6231197b8177/ast_serialize-0.10.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:45cfbde7e43a56a386b47f13b415d1fa4421b0a5e07ae0c84a7d883ac9aca4b2", size = 1462406, upload-time = "2026-09-07T10:22:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/74/08/1cef69ee4228cd82b9da6b5e29e7fcdd30a6b8b3d6332815781e09666b62/ast_serialize-0.10.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:b3a52baa4c457d17a5729aa9bd698da8e61731f1670b4fe33923da54daa480e3", size = 1566792, upload-time = "2026-09-07T10:22:34.381Z" }, + { url = "https://files.pythonhosted.org/packages/d2/01/a3d773d0fe1536485069953d54bece7d8a95aa688e64f5cd692f68949051/ast_serialize-0.10.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:c07cb3f354c234cfacf81da95194b085186d7f4c1f7ad06c7f04fb49e320ef6d", size = 1560873, upload-time = "2026-09-07T10:22:35.844Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ba/7fdda55197f06b2a1613d2ed7e647035b40b26b7aff62ac78e6728128ce9/ast_serialize-0.10.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:8c91b2bc42a12252be53966a1f1b14cc5c843dfca82bee3bce38f2e96b327aa6", size = 1693061, upload-time = "2026-09-07T10:22:37.355Z" }, + { url = "https://files.pythonhosted.org/packages/3a/29/630a7cafd2780651ed73d468518be9d08a6cc3e7c30fdc88c0b2b90c1c5a/ast_serialize-0.10.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:cb698ad6625a55d5d618c9d887423fd04d65dc5ad0f3ae5a8cbd96c46cea2438", size = 1487731, upload-time = "2026-09-07T10:22:39.403Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c3/84fc44b110ee10fb410d49f7f28686c4d23637161b6cb08775c1af8a4507/ast_serialize-0.10.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4496c0d20111a243525cb074a3e9a9313bbd7d3fb1d40ddee2ade9ef680b80c6", size = 1501731, upload-time = "2026-09-07T10:22:40.958Z" }, + { url = "https://files.pythonhosted.org/packages/59/9d/eedfd5a8cbeffd7172281e14d7a7e7640ed3997b3a09382c7756e39a2bbd/ast_serialize-0.10.0-cp39-abi3-win32.whl", hash = "sha256:ca5ff9030b426b245cf7897137872cdb6ebe43b2e4c5e04d5812ed24955a91ba", size = 1121201, upload-time = "2026-09-07T10:22:42.629Z" }, + { url = "https://files.pythonhosted.org/packages/a9/83/2db45120bd0ae66cfcd5e235d4dd29aec564e56440ebb17ec83428bb2685/ast_serialize-0.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:f1a8508054137a1cc67a64915ba588e75f03f546a9b16424aaf0cc9854b0a2a2", size = 1158429, upload-time = "2026-09-07T10:22:44.546Z" }, + { url = "https://files.pythonhosted.org/packages/53/f4/3850ee0050b68f3bd31c06a70cb065f7b129123e5795ff8a0ef642db71da/ast_serialize-0.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:354d12c7463266f2ca655af6f0333ee91ea98c875ffc5f47f10f03c7fe8a0667", size = 1130605, upload-time = "2026-09-07T10:22:46.082Z" }, ] [[package]] @@ -480,22 +480,22 @@ wheels = [ [[package]] name = "caio" -version = "0.12.2" +version = "0.12.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/c8/82b3c760141a1076408164b03e8789b51809add6aecd48aa9d7651cf6b59/caio-0.12.2.tar.gz", hash = "sha256:87a67c0dccc60e432888bd532ec504b66e124a5d8b391aab894583b55abd39ea", size = 80927, upload-time = "2026-08-04T14:43:33.726Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/c9/ac301b7f86ccf6ad02ee78eb953ce8f75175754cc5e76d398e447af42e3e/caio-0.12.4.tar.gz", hash = "sha256:32d8e9f3e2099c8db29446679252766c9bcd806eb88b4fb60ad274f73df2a5e9", size = 81006, upload-time = "2026-09-07T06:52:39.18Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/bc/b62bf048a6e11870291a24319ed027bdf658df9ba77d1ad762aa138e066b/caio-0.12.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2097cc0d19fa95e8d55aad770597bb0f76e4f70ed48278c965aa7c5b0b8c3bf5", size = 84702, upload-time = "2026-08-04T14:43:03.946Z" }, - { url = "https://files.pythonhosted.org/packages/f7/be/b40d55d793afcfa5bcdb32ade9289d9588e14e3026c2c87522e303cc6e8c/caio-0.12.2-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:2122dccbd1959b922543fc9f8a9d2af47bd5b59190d1ece2445d3d1b4d1be45f", size = 198292, upload-time = "2026-08-04T14:43:05.238Z" }, - { url = "https://files.pythonhosted.org/packages/f8/02/9bd2bca72bfa478337618eae88942c43c891ae225e11baeae275e5e5c6ab/caio-0.12.2-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:107e56554c179749de9440e1b5e5a19813572eebf3166e9dc3e5228b16966beb", size = 196207, upload-time = "2026-08-04T14:43:06.494Z" }, - { url = "https://files.pythonhosted.org/packages/48/9b/65f95efdd68b50b7a9f2555c93d9edc7da7aa5ae5e153163c41cf6fd5cd9/caio-0.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adc7785e61ff7cf372318f67ec65617eaa06975e20da177522665dca8be6ea5d", size = 195748, upload-time = "2026-08-04T14:43:07.893Z" }, - { url = "https://files.pythonhosted.org/packages/3c/16/6a5c010ca435a5184d11ca350874694ac19db249560126dc8df0f25791ce/caio-0.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07942d3b5999127ecb96256c38d5dbf49ed2864c087ed2a80b783901d0aa3ba1", size = 195835, upload-time = "2026-08-04T14:43:09.19Z" }, - { url = "https://files.pythonhosted.org/packages/4f/9b/31f0b49a2542ffa2f9d6140267e2b568e722a1feeb05cfbffea97666c62b/caio-0.12.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:40ebea9ebe3a3a66ae85fa00d4112d163654a33c82dcf9b26a99f7d30de13317", size = 84656, upload-time = "2026-08-04T14:43:10.513Z" }, - { url = "https://files.pythonhosted.org/packages/99/bc/62568d688af9712a34fe3f958d7a98c53bb2017e263260cd5deae67a90e9/caio-0.12.2-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:6003ec389a68d5ec8f089df82b2dc8915293dd630a4d11322d7e3455045981fd", size = 198443, upload-time = "2026-08-04T14:43:11.767Z" }, - { url = "https://files.pythonhosted.org/packages/a3/e4/5ed627860285612e5307f06c109913c5918c947fbc223b55599e484c64b0/caio-0.12.2-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:eee9376d0e2af25b6defc5bce39f6efa90521c803aaf12eba931bd898a397cfc", size = 196356, upload-time = "2026-08-04T14:43:13.206Z" }, - { url = "https://files.pythonhosted.org/packages/81/e2/2a8cfc6ba3ef3f19e7c778e9fb6f98600f0971cca78bbdfc23a413a66349/caio-0.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:78e3ccafc98e009fcb00a97ad441585551e52c0ae7ecc50427a3ccd9b11502fd", size = 195893, upload-time = "2026-08-04T14:43:14.649Z" }, - { url = "https://files.pythonhosted.org/packages/d1/87/77c40fb2301d0b5bb27c2e79ae42fce718ed75396d5fe3e1c09d8e1400b1/caio-0.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f2355db8917f5a0f3638bf332fe0d87549c80e978fca01db84a8a14b9df56a05", size = 195969, upload-time = "2026-08-04T14:43:15.946Z" }, - { url = "https://files.pythonhosted.org/packages/5e/b5/0ceca97eb546fe6bbace3399c8b11dfc503efcc7509d708a7a3f09ab50e9/caio-0.12.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8054cba5e7ee623bea34946e2b59eb7c7c2be8872d0a5d12215d6ff564938d5f", size = 78621, upload-time = "2026-08-04T14:43:17.316Z" }, - { url = "https://files.pythonhosted.org/packages/61/8a/71b0144f783468ba9f1bbf8a2f8e45c7d85ae31ec192f10650aa46f31702/caio-0.12.2-py3-none-any.whl", hash = "sha256:5233e797c9fe2b541914b1bc2e2df82677e2206b537e44e252188f3c2cbb0ea9", size = 62548, upload-time = "2026-08-04T14:43:32.394Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/7ddc8de48cd0142797db186c07e901005b966dc6e8a7852f0273d33e4840/caio-0.12.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:67b725641fea682a2e9b1d3b2a150d21f1a25383e3ec8351bd7677ff857fa982", size = 47927, upload-time = "2026-09-07T06:52:13.58Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b4/37556bde83253cf1ed071a754c8405d02b955febd021c400f0ffa6258d5f/caio-0.12.4-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:4854a029e359e5ddfb5589ab66157a34af6a0dda0acdaa43f5e640d60182c1ea", size = 161402, upload-time = "2026-09-07T06:52:14.581Z" }, + { url = "https://files.pythonhosted.org/packages/7a/11/519e40a1da4d18515ab5ddb54e75cdd896532339010f2dcee7e798e4edab/caio-0.12.4-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:f063624a98a64bab387430c5eaea61ef3825a98399104a297b8da4741c15139f", size = 159311, upload-time = "2026-09-07T06:52:15.853Z" }, + { url = "https://files.pythonhosted.org/packages/45/2a/0db30de6c2561e41721d94ac627aa65db61a403545e67914814d8c2b5305/caio-0.12.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a926d562f8c06767774a91239770ebaa93c10a24074e21ea741db208df9cc1d", size = 158910, upload-time = "2026-09-07T06:52:17.27Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e0/5eb43a89aecdf9d6f929b0080e49d92ccdf3f88bbc637353f52843b483af/caio-0.12.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:53605636e3b1eaeca368b475f1471ad9774736c6502a4b58b8b6b8d74b531770", size = 158973, upload-time = "2026-09-07T06:52:18.527Z" }, + { url = "https://files.pythonhosted.org/packages/11/ab/12e4896a1e74ff9a65c84894acdfc3b70b8b49e63874a37e0dffcc31f396/caio-0.12.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:134d9d145d75f454de5ece9e87595bad433639b51061b693bafc369f689f8742", size = 47934, upload-time = "2026-09-07T06:52:19.644Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d4/3a96f5537e3fa4e3256f93244fe883a5a4879f2b9e2d59d50151bb417f37/caio-0.12.4-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:27ec5671ac05650abc7ac1fb12e95ad09417ae495ce9be565927786688edd6c5", size = 161559, upload-time = "2026-09-07T06:52:20.637Z" }, + { url = "https://files.pythonhosted.org/packages/05/64/f33b9aaf7bc9bad988211d8e42b4b85c25a1d5dcc0906798aebe49bd421b/caio-0.12.4-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:1e1d5570fd0ec75b1b2c2877c5545ed9f0738b5d23b000e2b6a8fc830dc8b310", size = 159453, upload-time = "2026-09-07T06:52:22.011Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/3fc5e143728d99f8af11453789d755937fbe795a6c5c9c9ea529c91df519/caio-0.12.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e22ce2e69d94e4c80e0c11c871e547b3c2d147f977632894cb2527ddb6e87a8d", size = 159028, upload-time = "2026-09-07T06:52:23.225Z" }, + { url = "https://files.pythonhosted.org/packages/ca/8d/e7a165aa44bb2876bec96f7fb1995346d14e43d46237421382f8af4f37da/caio-0.12.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9dc0fd6f09ef72d18d3f43ca3d1140295222d925c583114ab9b1e8843a109a6e", size = 159088, upload-time = "2026-09-07T06:52:24.362Z" }, + { url = "https://files.pythonhosted.org/packages/6f/31/d31ed073a7f8e02d352b390ff556757ba82afbd747c68238b1bda638ce6f/caio-0.12.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:413565d77dfdf2dd841ac100571ef1cc710f9c56137312f3ec51356350b4f3a5", size = 41919, upload-time = "2026-09-07T06:52:25.5Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e6/07100a694613344d22958fb6d803fdf914ee9395278fe58ad1b1b9c59e52/caio-0.12.4-py3-none-any.whl", hash = "sha256:7a5e231bbf81eaed269f99afa9ef46457f51f9407952a1795c0795b3a62c23c0", size = 25628, upload-time = "2026-09-07T06:52:38.147Z" }, ] [[package]] @@ -2195,7 +2195,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.29.1" +version = "1.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2213,9 +2213,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/48/0bb26fdfe7ac16875f534a101ce2405eae192bdef37e7451f2f4507c13ec/mcp-1.29.1.tar.gz", hash = "sha256:1967ba4c315f7a375146209949f45950d18b0efd2f913d7cf3400bc723ee5f04", size = 646823, upload-time = "2026-08-24T18:30:41.161Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/93/0142dc84a666daf8ad51a34268f34c12fd6fda4f3810c4be2504eecc8212/mcp-1.30.0.tar.gz", hash = "sha256:445414625fce5c295faa505bb11bacece661ab6f4028d57c935db57820b7a3e4", size = 680511, upload-time = "2026-09-07T14:34:15.845Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/04/d6b4fb82eefe9e81807aabca1ac98f460ae0883974b83a997aaa20c52545/mcp-1.29.1-py3-none-any.whl", hash = "sha256:b6310eeb59153300c4ab8b9aec4c52f4819a2d6a8e429eb43d908bed7c783648", size = 224653, upload-time = "2026-08-24T18:30:39.573Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f4/e58bc33317c92a0203664daaf00bf6f41166cc0149e5d6870a03f7cd004a/mcp-1.30.0-py3-none-any.whl", hash = "sha256:666edb5009503e1047c9d60346a756f94b261f05cc2625f23d41c728ffc484d0", size = 234581, upload-time = "2026-09-07T14:34:14.266Z" }, ] [[package]] @@ -3541,11 +3541,11 @@ wheels = [ [[package]] name = "pypdf" -version = "6.17.0" +version = "6.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5d/dc/34857a5e31cf708c163929f61a9ba4bd357a8850e49fc4e846ced527b51f/pypdf-6.17.0.tar.gz", hash = "sha256:097ad0d829778ec5b615aeaa5c6da4b6cac4992f8fd80b56f98a1a8c006573bb", size = 7018352, upload-time = "2026-09-04T11:30:44.256Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/c3/9fa0666f280552bd3833562d985b785fce1ddb3804937edd2fd6a3f2bdb3/pypdf-6.18.0.tar.gz", hash = "sha256:ae58b7d93c22c169ffb02c3b06321c45c4f223b4916536568adb57d789d95d01", size = 7024871, upload-time = "2026-09-07T16:48:16.444Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/08/1e9731038124a9127e1d27848952b86fb32b2f45f8f1b94adc7f0817a6ac/pypdf-6.17.0-py3-none-any.whl", hash = "sha256:5bd827266a21553b74d910e350131a6227b72f2ab4209bf372814b8195fa11c5", size = 388051, upload-time = "2026-09-04T11:30:42.681Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a5/d5922a078c9a612327681d4793404f82998f4d66213add6fdc0659245856/pypdf-6.18.0-py3-none-any.whl", hash = "sha256:05b762b77bcb9dcb4a7c91fcf5dded585b25bee7269ab3d3001d7c55fa1b324b", size = 393848, upload-time = "2026-09-07T16:48:14.254Z" }, ] [[package]] @@ -3581,22 +3581,22 @@ wheels = [ [[package]] name = "pyroscope-io" -version = "1.2.1" +version = "1.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/d2/5fc44302f861eb2fd19bf7e56423c93e8867199c647337bb9849bc6d2929/pyroscope_io-1.2.1.tar.gz", hash = "sha256:c3236136dc086845d283fbeb434f5e22f5a7ddc0ff5a5b328752335d61ee1aaf", size = 74584, upload-time = "2026-07-27T11:39:44.57Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/b6/d9e545262fd93c46cc0714c3006c135fcd32e64331efcd391bc4dc8abe13/pyroscope_io-1.2.3.tar.gz", hash = "sha256:d7a2529efeaf8a0b7ac9326e5b8228b16f12171c6e6f07400ed65048b789fdae", size = 75094, upload-time = "2026-09-08T10:16:51.043Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/2c/cfaddac31c80b471cc896d3d09ee88d99779861a6b6dc3a7f7d43f12e39e/pyroscope_io-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:692091ae7d020ee76a16d9da2e213b69c20367d20032ecb643c23e3a5831f87d", size = 2113014, upload-time = "2026-07-27T11:39:18.237Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d5/188f433e9ab08973948fac72e6aacf2a641a69c45885bed982cc8a768e64/pyroscope_io-1.2.1-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:ceabd42b2d61876eb81668cb76bb04b911d62369645c1c4eb63c9d4ed5106b3c", size = 2196053, upload-time = "2026-07-27T11:39:19.594Z" }, - { url = "https://files.pythonhosted.org/packages/96/86/fa0bca1756f881b9960c460cef7604e9044bb84a68d9d76cbd3e39372cf3/pyroscope_io-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e64e2f34f340b163013b2942017d98bb0a4584e26ae64dfdba3f85304eea8efe", size = 5579568, upload-time = "2026-07-27T11:39:20.804Z" }, - { url = "https://files.pythonhosted.org/packages/de/1c/a7dac80341fe67ae775a33f9d0da2c04445479be3ce8a96e4b62df210c6e/pyroscope_io-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:16a39b80e5032fe978eaac6abf907e7b79128357adc0f0d8e7921e68bd57c4ea", size = 5123015, upload-time = "2026-07-27T11:39:22.185Z" }, - { url = "https://files.pythonhosted.org/packages/6d/90/049e9fda943f6fd01f035783c633d3e83a1a34aa00b1ef1eb325f0b0aa86/pyroscope_io-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:45f5d7ed4a7a158732d3c1634846011c382309720d891200f64aee3a11173418", size = 5510537, upload-time = "2026-07-27T11:39:23.633Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9b/f35ccd7935f87aa67a60e158b45f992e2e08243c5c6b432567cf7b1ef194/pyroscope_io-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7bc01f8b5c49efca59987194c28667559b1fbded965943d020d12aadc40d0c5c", size = 5239529, upload-time = "2026-07-27T11:39:25.532Z" }, - { url = "https://files.pythonhosted.org/packages/fc/11/8a3d16443d157f817975e61f2e97e894a6e2966cfe700cb900a4f16caf9e/pyroscope_io-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2ce28485fc46390c31300abde8c93b7dd6c042960deb1b729c2d001a8795516", size = 2112233, upload-time = "2026-07-27T11:39:26.889Z" }, - { url = "https://files.pythonhosted.org/packages/cd/34/640bcbeb50aec8dac27b4242b349a2a2eeb3c4ab8b62660ae9cb2dbe1cee/pyroscope_io-1.2.1-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:10b0e7a8040ed6b953c9920f2c463d6dddd4f26c01dcb1ffb696879c15cab218", size = 2195244, upload-time = "2026-07-27T11:39:28.397Z" }, - { url = "https://files.pythonhosted.org/packages/03/07/d3e7c279d44b88e94484453574ff837b0fecae3395a979d4def25b548234/pyroscope_io-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:655f8629f3f8c5b8c2bec105f0c19a69be4666ae5e452e4c910b2fa3de3452d6", size = 5578633, upload-time = "2026-07-27T11:39:29.805Z" }, - { url = "https://files.pythonhosted.org/packages/4d/59/9ce4cdafc9a31eda7aeb37c931367c61dab7846b56e396259273efa46e6f/pyroscope_io-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e232ea23cc756f6990a325230c6f22d7c380b012a3bd9dac476a338b1edd7d26", size = 5121946, upload-time = "2026-07-27T11:39:31.478Z" }, - { url = "https://files.pythonhosted.org/packages/19/80/cc8eb25b908e27fc7ef63b007eb226e3c31903fd9886ab18dea1fccda2c6/pyroscope_io-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35e57ab13d40d8af15821bc30720eb5c74949c169b649379ad114f7e1f75cd8f", size = 5510621, upload-time = "2026-07-27T11:39:32.939Z" }, - { url = "https://files.pythonhosted.org/packages/ad/9c/6452ac8356c4f7f11e67a96bc57062b813158e1315fad63ac81008d5ed79/pyroscope_io-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:31a2a694f507280a451fe082789599e68af48389c74b2c15d99715bd0d0f9f77", size = 5238044, upload-time = "2026-07-27T11:39:34.654Z" }, + { url = "https://files.pythonhosted.org/packages/22/79/df054a33fe90cab7d05c05ee0bae3820c5a2ca2f3ee13924caa27e9e421e/pyroscope_io-1.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0d9dc120c68a1af17dfe55933ee6e89d6ebc6c92c244235f51ee15eab4893efc", size = 2111877, upload-time = "2026-09-08T10:16:24.652Z" }, + { url = "https://files.pythonhosted.org/packages/50/b6/721cd69b0eb2ae2ac9d8c48abf4398b434e068fdf24d6847c2ec169b899a/pyroscope_io-1.2.3-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:1ffbf26ea19b05b6020ad5167802a1a6a8af0d02020a887172cecf74af2bba4b", size = 2195425, upload-time = "2026-09-08T10:16:25.899Z" }, + { url = "https://files.pythonhosted.org/packages/02/73/1489788e8fc3cd6c9a14b50cf7d40fe1cee276780893849572c62e908b6b/pyroscope_io-1.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1d06dcf3298e2ad5a3bd3efade9331e50291ad09b41e7643b0bcb2c574a3bc2b", size = 5581228, upload-time = "2026-09-08T10:16:27.222Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7d/ebccfa22e1fb1b2227df447130b6f8f5388366c2b6e9647b7259e6e9bda0/pyroscope_io-1.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:568a45945a7dedb33cc67657a06bc32085d11bd0b74adb1fc8a5e044cc7bf096", size = 5124659, upload-time = "2026-09-08T10:16:28.737Z" }, + { url = "https://files.pythonhosted.org/packages/ec/52/4d6088f857f00a57b93851a07f137985ce96c24961e1fac16709a6d14ded/pyroscope_io-1.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c47a3f798c83258189173de5332bd92d36e84c05b71fe36c9e3412ac291852d4", size = 5513055, upload-time = "2026-09-08T10:16:30.141Z" }, + { url = "https://files.pythonhosted.org/packages/65/22/dd430812a92c739ec046840e22032eeb40a4ad4a455c2ed1537f6e2aa60a/pyroscope_io-1.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c641d9c1378ac16ce8e4d081aa0d8203a71907540927dddeb17212ea65708820", size = 5243163, upload-time = "2026-09-08T10:16:31.858Z" }, + { url = "https://files.pythonhosted.org/packages/5e/f8/f7ba7b2d35cfbd2666a4d87258e8d5d39f36703eade6fa75f53568b63257/pyroscope_io-1.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dc258d7f9a3eda932bfb802da65dca5ff2599cf2b29c0804c18e3b81e0914820", size = 2111420, upload-time = "2026-09-08T10:16:33.643Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e4/480ddca9a12cfcb81dbfc5b6914de6f6c7d069bbc24dd18421569d2e7c7c/pyroscope_io-1.2.3-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:f85b4150d61d7e1fa1153fed60867ab27606ff9b0aec27b01d3dc29cae65cfbc", size = 2194392, upload-time = "2026-09-08T10:16:34.836Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9c/ee8b3e5546b1c369045eb9912a5f590a9f2a6daafc2f3ba5338cc37f1e5e/pyroscope_io-1.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:69c9f5a06f286edc1728c95b1d45cc868acbb471b6d8b49ee630814a564bbeba", size = 5580928, upload-time = "2026-09-08T10:16:36.261Z" }, + { url = "https://files.pythonhosted.org/packages/d5/23/60abf34c26eb21b02b5b7a3664d1891c50723934a587331acc948d768c4b/pyroscope_io-1.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:703e8fe769b26e41abd728bd3491c2f8adfc1843480dee7d82a94cb36b69dfa2", size = 5124040, upload-time = "2026-09-08T10:16:37.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/22/5d924b6c70a2ee30faaee4f842daf1b6c50676d70b827edc9f2686228aa8/pyroscope_io-1.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7584c53cef3416f75e40ce48b750f106e9a96135dda7ec7ae5441b6ef5fb5e0d", size = 5512810, upload-time = "2026-09-08T10:16:39.129Z" }, + { url = "https://files.pythonhosted.org/packages/88/79/0ddef490331ed7790989bdf8412a2136a382c842cf46fb662249bf7fccf8/pyroscope_io-1.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:06d8d392693452263ec14955322d4e46804136215830fc131fad4a12bb757116", size = 5241779, upload-time = "2026-09-08T10:16:40.631Z" }, ] [[package]] @@ -4135,15 +4135,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.68.1" +version = "2.69.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/e7/c504a4bd2d95df2e0ab73714a9161ff1cf6ff1486922685e5f46dfd9eba8/sentry_sdk-2.68.1.tar.gz", hash = "sha256:6a97895230b04bc35d4d8d2e51e3b9e21902dfb0086ccf1f131a80c15c7b997a", size = 1019262, upload-time = "2026-08-24T13:09:38.108Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/d4/911132f0ad673372159b4c47d3074444651b68c4c834fd5427f91285c66a/sentry_sdk-2.69.0.tar.gz", hash = "sha256:0cf7d29419145ab0250ea51363ad79fb7509996aaa35c5d5d2c3ed85b3b80a7d", size = 1042224, upload-time = "2026-09-08T08:16:29.258Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/28/465ad9382be98f2172e691f5836cf87f936773913ad7ab85ba1ba1d6706e/sentry_sdk-2.68.1-py3-none-any.whl", hash = "sha256:775b78871783a0ffd758276ad01b3bb2b1ebcdad8f9d2f0a7723f76b73c99b65", size = 520851, upload-time = "2026-08-24T13:09:36.186Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c4/11acfc646282156d8bec336db28143d33196531bd982fe20c2280f03abf6/sentry_sdk-2.69.0-py3-none-any.whl", hash = "sha256:3b92738027322061fbab34199102fcc0459ff9a5bf373ccca7091af9a5f07530", size = 528018, upload-time = "2026-09-08T08:16:27.203Z" }, ] [package.optional-dependencies] From 9685b580aa7f0b254fbbde7193952d9842c0f9c9 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Tue, 8 Sep 2026 16:03:47 +0200 Subject: [PATCH 035/120] LCORE-3933: Regenerated documentation --- docs/models/common.json | 116 +++++- docs/models/common.md | 56 ++- docs/models/requests.json | 204 +++++++++- docs/models/requests.md | 93 ++++- docs/models/responses.puml | 4 +- docs/models/responses.svg | 212 +++++----- docs/models/successful_responses.json | 363 +++++++++++++++++- docs/models/successful_responses.md | 170 +++++++- src/README.md | 8 +- src/observability/README.md | 4 + .../llamastack/README.md | 2 + src/utils/README.md | 12 +- src/utils/dumpers/README.md | 4 + tests/unit/README.md | 8 +- tests/unit/models/config/README.md | 8 +- .../llamastack/README.md | 2 + tests/unit/utils/README.md | 12 +- 17 files changed, 1113 insertions(+), 165 deletions(-) create mode 100644 src/pydantic_ai_lightspeed/llamastack/README.md create mode 100644 tests/unit/pydantic_ai_lightspeed/llamastack/README.md diff --git a/docs/models/common.json b/docs/models/common.json index e05889c53..0bbad2b17 100644 --- a/docs/models/common.json +++ b/docs/models/common.json @@ -1326,7 +1326,7 @@ "type": "object" }, "OpenAIResponseOutputMessageWebSearchToolCall": { - "description": "Web search tool call output message for OpenAI responses.\n\n:param id: Unique identifier for this tool call\n:param status: Current status of the web search operation\n:param type: Tool call type identifier, always \"web_search_call\"", + "description": "Web search tool call output message for OpenAI responses.", "properties": { "id": { "title": "Id", @@ -1341,6 +1341,24 @@ "default": "web_search_call", "title": "Type", "type": "string" + }, + "action": { + "anyOf": [ + { + "$ref": "`#/components/schemas/`WebSearchActionSearch" + }, + { + "$ref": "`#/components/schemas/`WebSearchActionOpenPage" + }, + { + "$ref": "`#/components/schemas/`WebSearchActionFind" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Action" } }, "required": [ @@ -2020,6 +2038,102 @@ }, "title": "TurnSummary", "type": "object" + }, + "WebSearchActionFind": { + "description": "Web search action: searches for a pattern within a loaded page.", + "properties": { + "type": { + "const": "find_in_page", + "default": "find_in_page", + "title": "Type", + "type": "string" + }, + "url": { + "title": "Url", + "type": "string" + }, + "pattern": { + "title": "Pattern", + "type": "string" + } + }, + "required": [ + "url", + "pattern" + ], + "title": "WebSearchActionFind", + "type": "object" + }, + "WebSearchActionOpenPage": { + "description": "Web search action: opens a specific URL from search results.", + "properties": { + "type": { + "const": "open_page", + "default": "open_page", + "title": "Type", + "type": "string" + }, + "url": { + "type": "string", + "nullable": true, + "default": null, + "title": "Url" + } + }, + "title": "WebSearchActionOpenPage", + "type": "object" + }, + "WebSearchActionSearch": { + "description": "Web search action: performs a search query.", + "properties": { + "type": { + "const": "search", + "default": "search", + "title": "Type", + "type": "string" + }, + "query": { + "title": "Query", + "type": "string" + }, + "queries": { + "type": "array", + "nullable": true, + "default": null, + "title": "Queries" + }, + "sources": { + "type": "array", + "nullable": true, + "default": null, + "title": "Sources" + } + }, + "required": [ + "query" + ], + "title": "WebSearchActionSearch", + "type": "object" + }, + "WebSearchSource": { + "description": "A source URL returned by a web search action.", + "properties": { + "type": { + "const": "url", + "default": "url", + "title": "Type", + "type": "string" + }, + "url": { + "title": "Url", + "type": "string" + } + }, + "required": [ + "url" + ], + "title": "WebSearchSource", + "type": "object" } } }, diff --git a/docs/models/common.md b/docs/models/common.md index 7a282a985..49bf3113c 100644 --- a/docs/models/common.md +++ b/docs/models/common.md @@ -623,16 +623,13 @@ A summary of reasoning output from the model. Web search tool call output message for OpenAI responses. -:param id: Unique identifier for this tool call -:param status: Current status of the web search operation -:param type: Tool call type identifier, always "web_search_call" - | Field | Type | Description | |-------|------|-------------| | id | string | | | status | string | | | type | string | | +| action | | | ## OpenAITokenLogProb @@ -906,3 +903,54 @@ Summary of a turn in OGX. | output_items | array | Structured response output items, captured for compacted-mode turn persistence (LCORE-1572). Empty on the non-compacted path. | | partial_tokens | array | Accumulated text deltas during streaming, used to reconstruct partial content on interruption. | | next_chunk_id | integer | Next monotonic SSE chunk index, kept in sync with the inner generator so the interrupt handler can emit a sequentially valid id. | + + +## WebSearchActionFind + + +Web search action: searches for a pattern within a loaded page. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| url | string | | +| pattern | string | | + + +## WebSearchActionOpenPage + + +Web search action: opens a specific URL from search results. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| url | string | | + + +## WebSearchActionSearch + + +Web search action: performs a search query. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| query | string | | +| queries | array | | +| sources | array | | + + +## WebSearchSource + + +A source URL returned by a web search action. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| url | string | | diff --git a/docs/models/requests.json b/docs/models/requests.json index e16154aaa..4a56726b3 100644 --- a/docs/models/requests.json +++ b/docs/models/requests.json @@ -1052,7 +1052,7 @@ "type": "object" }, "OpenAIResponseInputToolWebSearch": { - "description": "Web search tool configuration for OpenAI response inputs.\n\n:param type: Web search tool type variant to use\n:param search_context_size: (Optional) Size of search context, must be \"low\", \"medium\", or \"high\"", + "description": "Web search tool configuration for OpenAI response inputs.", "properties": { "type": { "anyOf": [ @@ -1079,8 +1079,30 @@ "search_context_size": { "type": "string", "nullable": true, - "default": "medium", + "default": null, "title": "Search Context Size" + }, + "filters": { + "anyOf": [ + { + "$ref": "`#/components/schemas/`WebSearchFilters" + }, + { + "type": "null" + } + ], + "default": null + }, + "user_location": { + "anyOf": [ + { + "$ref": "`#/components/schemas/`WebSearchUserLocation" + }, + { + "type": "null" + } + ], + "default": null } }, "title": "OpenAIResponseInputToolWebSearch", @@ -1601,7 +1623,7 @@ "type": "object" }, "OpenAIResponseOutputMessageWebSearchToolCall": { - "description": "Web search tool call output message for OpenAI responses.\n\n:param id: Unique identifier for this tool call\n:param status: Current status of the web search operation\n:param type: Tool call type identifier, always \"web_search_call\"", + "description": "Web search tool call output message for OpenAI responses.", "properties": { "id": { "title": "Id", @@ -1616,6 +1638,24 @@ "default": "web_search_call", "title": "Type", "type": "string" + }, + "action": { + "anyOf": [ + { + "$ref": "`#/components/schemas/`WebSearchActionSearch" + }, + { + "$ref": "`#/components/schemas/`WebSearchActionOpenPage" + }, + { + "$ref": "`#/components/schemas/`WebSearchActionFind" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Action" } }, "required": [ @@ -1660,6 +1700,14 @@ "default": null, "title": "Effort" }, + "generate_summary": { + "type": "string", + "nullable": true, + "default": null, + "deprecated": true, + "description": "Deprecated: use 'summary' instead.", + "title": "Generate Summary" + }, "summary": { "type": "string", "nullable": true, @@ -1688,7 +1736,7 @@ "verbosity": { "type": "string", "nullable": true, - "default": null, + "default": "medium", "title": "Verbosity" } }, @@ -2610,7 +2658,7 @@ "type": "object" }, "SearchRankingOptions": { - "description": "Options for ranking and filtering search results.\n\nThis class configures how search results are ranked and filtered. You can use algorithm-based\nrerankers (weighted, RRF) or neural rerankers. Defaults from VectorStoresConfig are\nused when parameters are not provided.\n\nExamples:\n # Weighted ranker with custom alpha\n SearchRankingOptions(ranker=\"weighted\", alpha=0.7)\n\n # RRF ranker with custom impact factor\n SearchRankingOptions(ranker=\"rrf\", impact_factor=50.0)\n\n # Use config defaults (just specify ranker type)\n SearchRankingOptions(ranker=\"weighted\") # Uses alpha from VectorStoresConfig\n\n # Score threshold filtering\n SearchRankingOptions(ranker=\"weighted\", score_threshold=0.5)\n\n:param ranker: (Optional) Name of the ranking algorithm to use. Supported values:\n - \"weighted\": Weighted combination of vector and keyword scores\n - \"rrf\": Reciprocal Rank Fusion algorithm\n - \"neural\": Neural reranking model (requires model parameter)\n Note: For OpenAI API compatibility, any string value is accepted, but only the above values are supported.\n:param score_threshold: (Optional) Minimum relevance score threshold for results. Default: 0.0\n:param alpha: (Optional) Weight factor for weighted ranker (0-1).\n - 0.0 = keyword only\n - 0.5 = equal weight (default)\n - 1.0 = vector only\n Only used when ranker=\"weighted\" and weights is not provided.\n Falls back to VectorStoresConfig.chunk_retrieval_params.weighted_search_alpha if not provided.\n:param impact_factor: (Optional) Impact factor (k) for RRF algorithm.\n Lower values emphasize higher-ranked results. Default: 60.0 (optimal from research).\n Only used when ranker=\"rrf\".\n Falls back to VectorStoresConfig.chunk_retrieval_params.rrf_impact_factor if not provided.\n:param weights: (Optional) Dictionary of weights for combining different signal types.\n Keys can be \"vector\", \"keyword\", \"neural\". Values should sum to 1.0.\n Used when combining algorithm-based reranking with neural reranking.\n Example: {\"vector\": 0.3, \"keyword\": 0.3, \"neural\": 0.4}\n:param model: (Optional) Model identifier for neural reranker (e.g., \"transformers/Qwen/Qwen3-Reranker-0.6B\").\n Required when ranker=\"neural\" or when weights contains \"neural\".", + "description": "Options for ranking and filtering search results.\n\nThis class configures how search results are ranked and filtered. You can use algorithm-based\nrerankers (weighted, RRF) or neural rerankers. Defaults from VectorStoresConfig are\nused when parameters are not provided.\n\nExamples:\n # Weighted ranker with custom alpha\n SearchRankingOptions(ranker=\"weighted\", alpha=0.7)\n\n # RRF ranker with custom impact factor\n SearchRankingOptions(ranker=\"rrf\", impact_factor=50.0)\n\n # Use config defaults (just specify ranker type)\n SearchRankingOptions(ranker=\"weighted\") # Uses alpha from VectorStoresConfig\n\n # Score threshold filtering\n SearchRankingOptions(ranker=\"weighted\", score_threshold=0.5)\n\n:param ranker: (Optional) Name of the ranking algorithm to use. Supported values:\n - \"weighted\": Weighted combination of vector and keyword scores\n - \"rrf\": Reciprocal Rank Fusion algorithm\n - \"neural\": Neural reranking model (requires model parameter)\n Note: For OpenAI API compatibility, any string value is accepted, but only the above values are supported.\n:param score_threshold: (Optional) Minimum relevance score threshold for results. Default: 0.0\n:param alpha: (Optional) Weight factor for weighted ranker (0-1).\n - 0.0 = keyword only\n - 0.5 = equal weight (default)\n - 1.0 = vector only\n Only used when ranker=\"weighted\" and weights is not provided.\n Falls back to VectorStoresConfig.chunk_retrieval_params.weighted_search_alpha if not provided.\n:param impact_factor: (Optional) Impact factor (k) for RRF algorithm.\n Lower values emphasize higher-ranked results. Default: 60.0 (optimal from research).\n Only used when ranker=\"rrf\".\n Falls back to VectorStoresConfig.chunk_retrieval_params.rrf_impact_factor if not provided.\n:param weights: (Optional) Dictionary of weights for combining different signal types.\n Keys can be \"vector\", \"keyword\", \"neural\". Values should sum to 1.0.\n Used when combining algorithm-based reranking with neural reranking.\n Example: {\"vector\": 0.3, \"keyword\": 0.3, \"neural\": 0.4}\n:param model: (Optional) Model identifier for neural reranker\n (e.g., \"sentence-transformers/Qwen/Qwen3-Reranker-0.6B\"). Required when ranker=\"neural\" or when\n weights contains \"neural\".", "properties": { "ranker": { "type": "string", @@ -2940,6 +2988,152 @@ }, "title": "VectorStoreUpdateRequest", "type": "object" + }, + "WebSearchActionFind": { + "description": "Web search action: searches for a pattern within a loaded page.", + "properties": { + "type": { + "const": "find_in_page", + "default": "find_in_page", + "title": "Type", + "type": "string" + }, + "url": { + "title": "Url", + "type": "string" + }, + "pattern": { + "title": "Pattern", + "type": "string" + } + }, + "required": [ + "url", + "pattern" + ], + "title": "WebSearchActionFind", + "type": "object" + }, + "WebSearchActionOpenPage": { + "description": "Web search action: opens a specific URL from search results.", + "properties": { + "type": { + "const": "open_page", + "default": "open_page", + "title": "Type", + "type": "string" + }, + "url": { + "type": "string", + "nullable": true, + "default": null, + "title": "Url" + } + }, + "title": "WebSearchActionOpenPage", + "type": "object" + }, + "WebSearchActionSearch": { + "description": "Web search action: performs a search query.", + "properties": { + "type": { + "const": "search", + "default": "search", + "title": "Type", + "type": "string" + }, + "query": { + "title": "Query", + "type": "string" + }, + "queries": { + "type": "array", + "nullable": true, + "default": null, + "title": "Queries" + }, + "sources": { + "type": "array", + "nullable": true, + "default": null, + "title": "Sources" + } + }, + "required": [ + "query" + ], + "title": "WebSearchActionSearch", + "type": "object" + }, + "WebSearchFilters": { + "description": "Domain filters for web search results.", + "properties": { + "allowed_domains": { + "type": "array", + "nullable": true, + "default": null, + "title": "Allowed Domains" + } + }, + "title": "WebSearchFilters", + "type": "object" + }, + "WebSearchSource": { + "description": "A source URL returned by a web search action.", + "properties": { + "type": { + "const": "url", + "default": "url", + "title": "Type", + "type": "string" + }, + "url": { + "title": "Url", + "type": "string" + } + }, + "required": [ + "url" + ], + "title": "WebSearchSource", + "type": "object" + }, + "WebSearchUserLocation": { + "description": "Approximate user location to refine web search results.", + "properties": { + "type": { + "const": "approximate", + "default": "approximate", + "title": "Type", + "type": "string" + }, + "city": { + "type": "string", + "nullable": true, + "default": null, + "title": "City" + }, + "country": { + "type": "string", + "nullable": true, + "default": null, + "title": "Country" + }, + "region": { + "type": "string", + "nullable": true, + "default": null, + "title": "Region" + }, + "timezone": { + "type": "string", + "nullable": true, + "default": null, + "title": "Timezone" + } + }, + "title": "WebSearchUserLocation", + "type": "object" } } }, diff --git a/docs/models/requests.md b/docs/models/requests.md index 3579a66bc..baf8562e7 100644 --- a/docs/models/requests.md +++ b/docs/models/requests.md @@ -507,14 +507,13 @@ Function tool configuration for OpenAI response inputs. Web search tool configuration for OpenAI response inputs. -:param type: Web search tool type variant to use -:param search_context_size: (Optional) Size of search context, must be "low", "medium", or "high" - | Field | Type | Description | |-------|------|-------------| | type | | | | search_context_size | string | | +| filters | | | +| user_location | | | ## OpenAIResponseMCPApprovalRequest @@ -732,16 +731,13 @@ A summary of reasoning output from the model. Web search tool call output message for OpenAI responses. -:param id: Unique identifier for this tool call -:param status: Current status of the web search operation -:param type: Tool call type identifier, always "web_search_call" - | Field | Type | Description | |-------|------|-------------| | id | string | | | status | string | | | type | string | | +| action | | | ## OpenAIResponsePrompt @@ -776,6 +772,7 @@ Controls how much reasoning the model performs before generating a response. | Field | Type | Description | |-------|------|-------------| | effort | string | | +| generate_summary | string | Deprecated: use 'summary' instead. | | summary | string | Summary mode for reasoning output. One of 'auto', 'concise', or 'detailed'. | @@ -1174,8 +1171,9 @@ Examples: Keys can be "vector", "keyword", "neural". Values should sum to 1.0. Used when combining algorithm-based reranking with neural reranking. Example: {"vector": 0.3, "keyword": 0.3, "neural": 0.4} -:param model: (Optional) Model identifier for neural reranker (e.g., "transformers/Qwen/Qwen3-Reranker-0.6B"). - Required when ranker="neural" or when weights contains "neural". +:param model: (Optional) Model identifier for neural reranker + (e.g., "sentence-transformers/Qwen/Qwen3-Reranker-0.6B"). Required when ranker="neural" or when + weights contains "neural". | Field | Type | Description | @@ -1279,3 +1277,80 @@ Attributes: | name | string | New name for the vector store | | expires_at | integer | Unix timestamp when the vector store should expire | | metadata | object | Metadata dictionary for storing session information | + + +## WebSearchActionFind + + +Web search action: searches for a pattern within a loaded page. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| url | string | | +| pattern | string | | + + +## WebSearchActionOpenPage + + +Web search action: opens a specific URL from search results. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| url | string | | + + +## WebSearchActionSearch + + +Web search action: performs a search query. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| query | string | | +| queries | array | | +| sources | array | | + + +## WebSearchFilters + + +Domain filters for web search results. + + +| Field | Type | Description | +|-------|------|-------------| +| allowed_domains | array | | + + +## WebSearchSource + + +A source URL returned by a web search action. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| url | string | | + + +## WebSearchUserLocation + + +Approximate user location to refine web search results. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| city | string | | +| country | string | | +| region | string | | +| timezone | string | | diff --git a/docs/models/responses.puml b/docs/models/responses.puml index eb5f3eac6..eb069dbf6 100644 --- a/docs/models/responses.puml +++ b/docs/models/responses.puml @@ -98,9 +98,9 @@ class "ForbiddenResponse" as src.models.api.responses.error.forbidden.ForbiddenR saved_prompt(action: str, resource_id: str, user_id: str) -> Self } class "InfoResponse" as src.models.api.responses.successful.probes.InfoResponse { - ogx_version : Optional[str] model_config : dict name : Optional[str] + ogx_version : Optional[str] service_version : Optional[str] } class "InternalServerErrorResponse" as src.models.api.responses.error.internal.InternalServerErrorResponse { @@ -347,7 +347,7 @@ class "VectorStoreFileDeleteResponse" as src.models.api.responses.successful.vec resource_name : ClassVar[str] } class "VectorStoreFileResponse" as src.models.api.responses.successful.vector_stores.VectorStoreFileResponse { - attributes : Optional[dict[str, str | float | bool]] + attributes : Optional[Mapping[str, Any]] id : Optional[str] last_error : Optional[str] model_config : dict diff --git a/docs/models/responses.svg b/docs/models/responses.svg index eba0ae5fb..d3f41457c 100644 --- a/docs/models/responses.svg +++ b/docs/models/responses.svg @@ -227,79 +227,79 @@ - - - - InfoResponse - - ogx_version : Optional[str] - model_config : dict - name : Optional[str] - service_version : Optional[str] - + + + + InfoResponse + + model_config : dict + name : Optional[str] + ogx_version : Optional[str] + service_version : Optional[str] + - - - - InternalServerErrorResponse - - description : ClassVar[str] - model_config : dict - - cache_unavailable() -> Self - configuration_not_loaded() -> Self - database_error() -> Self - feedback_path_invalid(path: str) -> Self - generic() -> Self - mcp_server_registration_failed() -> Self - query_failed(cause: str) -> Self + + + + InternalServerErrorResponse + + description : ClassVar[str] + model_config : dict + + cache_unavailable() -> Self + configuration_not_loaded() -> Self + database_error() -> Self + feedback_path_invalid(path: str) -> Self + generic() -> Self + mcp_server_registration_failed() -> Self + query_failed(cause: str) -> Self - - - - LivenessResponse - - alive : Optional[bool] - model_config : dict - + + + + LivenessResponse + + alive : Optional[bool] + model_config : dict + - - - - MCPClientAuthOptionsResponse - - model_config : dict - servers : Optional[list[MCPServerAuthInfo]] - + + + + MCPClientAuthOptionsResponse + + model_config : dict + servers : Optional[list[MCPServerAuthInfo]] + - - - - MCPServerDeleteResponse - - model_config : dict - name : Optional[str] - resource_name : ClassVar[str] - + + + + MCPServerDeleteResponse + + model_config : dict + name : Optional[str] + resource_name : ClassVar[str] + - - - - MCPServerListResponse - - model_config : dict - servers : Optional[list[MCPServerInfo]] - + + + + MCPServerListResponse + + model_config : dict + servers : Optional[list[MCPServerInfo]] + @@ -749,62 +749,62 @@ - - - - VectorStoreFileResponse - - attributes : Optional[dict[str, str | float | bool]] - id : Optional[str] - last_error : Optional[str] - model_config : dict - object : Optional[str] - status : Optional[str] - vector_store_id : Optional[str] - + + + + VectorStoreFileResponse + + attributes : Optional[Mapping[str, Any]] + id : Optional[str] + last_error : Optional[str] + model_config : dict + object : Optional[str] + status : Optional[str] + vector_store_id : Optional[str] + - - - - VectorStoreFilesListResponse - - data : Optional[list[VectorStoreFileResponse]] - model_config : dict - object : Optional[str] - + + + + VectorStoreFilesListResponse + + data : Optional[list[VectorStoreFileResponse]] + model_config : dict + object : Optional[str] + - - - - VectorStoreResponse - - created_at : Optional[int] - expires_at : Optional[int] - id : Optional[str] - last_active_at : Optional[int] - metadata : Optional[dict[str, Any]] - model_config : dict - name : Optional[str] - status : Optional[str] - usage_bytes : Optional[int] - + + + + VectorStoreResponse + + created_at : Optional[int] + expires_at : Optional[int] + id : Optional[str] + last_active_at : Optional[int] + metadata : Optional[dict[str, Any]] + model_config : dict + name : Optional[str] + status : Optional[str] + usage_bytes : Optional[int] + - - - - VectorStoresListResponse - - data : Optional[list[VectorStoreResponse]] - model_config : dict - object : Optional[str] - - - + + + + VectorStoresListResponse + + data : Optional[list[VectorStoreResponse]] + model_config : dict + object : Optional[str] + + + diff --git a/docs/models/successful_responses.json b/docs/models/successful_responses.json index 8fd4b10f3..ce326e5af 100644 --- a/docs/models/successful_responses.json +++ b/docs/models/successful_responses.json @@ -863,6 +863,7 @@ "items": { "discriminator": { "mapping": { + "granite_guardian": "`#/components/schemas/`GraniteGuardianShieldConfiguration", "question_validity": "`#/components/schemas/`QuestionValidityShieldConfiguration", "redaction": "`#/components/schemas/`RedactionShieldConfiguration" }, @@ -874,6 +875,9 @@ }, { "$ref": "`#/components/schemas/`RedactionShieldConfiguration" + }, + { + "$ref": "`#/components/schemas/`GraniteGuardianShieldConfiguration" } ] }, @@ -1769,6 +1773,96 @@ "title": "FileResponse", "type": "object" }, + "GraniteGuardianConfig": { + "additionalProperties": false, + "description": "Configuration for the Granite Guardian moderation guardrail.", + "properties": { + "url": { + "description": "The model_id to use for the guard", + "title": "Base URL", + "type": "string" + }, + "api_key": { + "type": "string", + "nullable": true, + "default": null, + "description": "API key for the inference", + "title": "Granite Guardian API key" + }, + "max_retries": { + "default": 2, + "description": "Maximun number of retires", + "minimum": 0, + "maximum": 5, + "title": "Max retries", + "type": "integer" + }, + "timeout": { + "default": 30, + "description": "Request timeout in seconds", + "minimum": 5, + "maximum": 300, + "title": "Timeout", + "type": "integer" + }, + "verify_ssl": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ], + "default": true, + "description": "SSL certificate verification. Can be:\n - True: Verify using system CA bundle (default, recommended)\n - False: Disable verification (insecure, for dev only)\n - str: Path to custom CA bundle file (for internal PKI)", + "title": "Verify SSL" + }, + "risks": { + "description": "Risks to be considered while applying this guradrail", + "items": { + "$ref": "`#/components/schemas/`RiskDefinition" + }, + "title": "Defined risks", + "type": "array" + } + }, + "required": [ + "url", + "risks" + ], + "title": "GraniteGuardianConfig", + "type": "object" + }, + "GraniteGuardianShieldConfiguration": { + "additionalProperties": false, + "description": "Configuration for a named Granite Guardian guardrail shield.\n\nAttributes:\n name: Unique, user-facing name identifying this shield instance.\n provider_id: Discriminator identifying this as a granite-guardian shield.\n config: Granite-guardian-specific configuration.", + "properties": { + "name": { + "description": "Unique, user-facing name identifying this shield instance.", + "title": "Shield name", + "type": "string" + }, + "provider_id": { + "const": "granite_guardian", + "description": "Discriminator identifying this as a granite-guardian shield.", + "title": "Shield provider id", + "type": "string" + }, + "config": { + "$ref": "`#/components/schemas/`GraniteGuardianConfig", + "description": "Granite-guardian-specific configuration for this shield", + "title": "Shield configuration" + } + }, + "required": [ + "name", + "provider_id", + "config" + ], + "title": "GraniteGuardianShieldConfiguration", + "type": "object" + }, "HealthStatus": { "description": "Health status enum for provider and service health checks.\n\nThis enum serves two purposes:\n\n1. Provider-level health (returned by OGX providers):\n - OK: Provider is healthy and operational\n - ERROR: Provider is unhealthy or failed health check\n - NOT_IMPLEMENTED: Provider does not implement health checks\n - UNKNOWN: Fallback when provider status cannot be determined\n\n2. Service-level health (overall LCORE status):\n - HEALTHY: All systems operational, LLS connected, all providers healthy\n - DEGRADED: Service running with reduced functionality (e.g., LLS unavailable)\n - UNHEALTHY: Service connected but one or more providers are unhealthy", "enum": [ @@ -2433,7 +2527,7 @@ }, "OgxConfiguration": { "additionalProperties": false, - "description": "OGX configuration.\n\nOGX is a comprehensive system that provides a uniform set of tools\nfor building, scaling, and deploying generative AI applications, enabling\ndevelopers to create, integrate, and orchestrate multiple AI services and\ncapabilities into an adaptable setup.\n\nUseful resources:\n\n - [OGX](https://ogx-ai.github.io/)\n - [Python OGX client](https://github.com/ogx-ai/ogx-client-python)\n - [Build AI Applications with OGX](https://ogx-ai.github.io/)", + "description": "OGX configuration.\n\nOGX is a comprehensive system that provides a uniform set of tools\nfor building, scaling, and deploying generative AI applications, enabling\ndevelopers to create, integrate, and orchestrate multiple AI services and\ncapabilities into an adaptable setup.\n\nUseful resources:\n\n - [OGX](https://ogx-ai.github.io/)\n - [Python OGX client](https://github.com/ogx-ai/ogx-client-python)\n - [Build AI Applications with OGX](https://ogx-ai.github.io/docs/building_applications)", "properties": { "url": { "type": "string", @@ -3057,7 +3151,7 @@ "type": "object" }, "OpenAIResponseInputToolWebSearch": { - "description": "Web search tool configuration for OpenAI response inputs.\n\n:param type: Web search tool type variant to use\n:param search_context_size: (Optional) Size of search context, must be \"low\", \"medium\", or \"high\"", + "description": "Web search tool configuration for OpenAI response inputs.", "properties": { "type": { "anyOf": [ @@ -3084,8 +3178,30 @@ "search_context_size": { "type": "string", "nullable": true, - "default": "medium", + "default": null, "title": "Search Context Size" + }, + "filters": { + "anyOf": [ + { + "$ref": "`#/components/schemas/`WebSearchFilters" + }, + { + "type": "null" + } + ], + "default": null + }, + "user_location": { + "anyOf": [ + { + "$ref": "`#/components/schemas/`WebSearchUserLocation" + }, + { + "type": "null" + } + ], + "default": null } }, "title": "OpenAIResponseInputToolWebSearch", @@ -3569,7 +3685,7 @@ "type": "object" }, "OpenAIResponseOutputMessageWebSearchToolCall": { - "description": "Web search tool call output message for OpenAI responses.\n\n:param id: Unique identifier for this tool call\n:param status: Current status of the web search operation\n:param type: Tool call type identifier, always \"web_search_call\"", + "description": "Web search tool call output message for OpenAI responses.", "properties": { "id": { "title": "Id", @@ -3584,6 +3700,24 @@ "default": "web_search_call", "title": "Type", "type": "string" + }, + "action": { + "anyOf": [ + { + "$ref": "`#/components/schemas/`WebSearchActionSearch" + }, + { + "$ref": "`#/components/schemas/`WebSearchActionOpenPage" + }, + { + "$ref": "`#/components/schemas/`WebSearchActionFind" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Action" } }, "required": [ @@ -3628,6 +3762,14 @@ "default": null, "title": "Effort" }, + "generate_summary": { + "type": "string", + "nullable": true, + "default": null, + "deprecated": true, + "description": "Deprecated: use 'summary' instead.", + "title": "Generate Summary" + }, "summary": { "type": "string", "nullable": true, @@ -3656,7 +3798,7 @@ "verbosity": { "type": "string", "nullable": true, - "default": null, + "default": "medium", "title": "Verbosity" } }, @@ -5576,6 +5718,69 @@ "title": "RetrievalStrategyConfiguration", "type": "object" }, + "RiskDefinition": { + "additionalProperties": false, + "description": "Definition for a custom risk category.\n\nCustom risks allow applications to add use-case-specific safety checks\nbeyond the standard harm, jailbreak, leetspeak, amnesia, and\nhistory_politics checks.\nExample:\n liability_risk = RiskDefinition(\n name=\"liability\",\n description=\"Content requesting legal, medical, or financial advice\",\n threshold=0.55,\n points=[\"input\"],\n )\n pii_risk = RiskDefinition(\n name=\"pii_request\",\n description=\"User is asking the AI to reveal personal information\",\n threshold=0.50,\n points=[\"input\", \"tool\"],\n )\nNote:\n To enable think mode (detailed reasoning) for a risk, add the risk name\n to the `thinking_enabled` list in `ModerationConfig`. Do not set\n `enable_thinking` directly - it is managed internally.", + "properties": { + "name": { + "description": "Unique identifier for this risk (e.g., 'liability', 'competitor_mention')", + "title": "Risk name", + "type": "string" + }, + "description": { + "description": "Risk definition text passed to Granite Guardian as custom_criteria", + "title": "Rist description", + "type": "string" + }, + "threshold": { + "default": 0.65, + "description": "Score threshold for flagging (lower = more sensitive)", + "maximum": 1.0, + "minimum": 0.0, + "title": "Risk threshold", + "type": "number" + }, + "enabled": { + "default": true, + "description": "Whether to run this check", + "title": "Risk enabled", + "type": "boolean" + }, + "enable_thinking": { + "default": false, + "description": "Internal field - set via ModerationConfig.thinking_enabled list, not directly. When True, Granite Guardian provides detailed reasoning before scoring.", + "title": "Risk enable thinking", + "type": "boolean" + }, + "points": { + "description": "Where this risk is evaluated: `input` (user message), `output` (model response), or `tool` (tool/MCP content).", + "items": { + "enum": [ + "input", + "output", + "tool" + ], + "type": "string" + }, + "minItems": 1, + "title": "Guardrail points", + "type": "array" + }, + "violation_message": { + "description": "Message to be displayed when this risk is violated", + "title": "Violation message", + "type": "string" + } + }, + "required": [ + "name", + "description", + "points", + "violation_message" + ], + "title": "RiskDefinition", + "type": "object" + }, "RlsapiV1Configuration": { "additionalProperties": false, "description": "Configuration for the rlsapi v1 /infer endpoint.\n\nSettings specific to the RHEL Lightspeed Command Line Assistant (CLA)\nstateless inference endpoint. Kept separate from shared configuration\nsections so that CLA-specific options do not affect other endpoints.", @@ -5931,7 +6136,7 @@ "type": "object" }, "SearchRankingOptions": { - "description": "Options for ranking and filtering search results.\n\nThis class configures how search results are ranked and filtered. You can use algorithm-based\nrerankers (weighted, RRF) or neural rerankers. Defaults from VectorStoresConfig are\nused when parameters are not provided.\n\nExamples:\n # Weighted ranker with custom alpha\n SearchRankingOptions(ranker=\"weighted\", alpha=0.7)\n\n # RRF ranker with custom impact factor\n SearchRankingOptions(ranker=\"rrf\", impact_factor=50.0)\n\n # Use config defaults (just specify ranker type)\n SearchRankingOptions(ranker=\"weighted\") # Uses alpha from VectorStoresConfig\n\n # Score threshold filtering\n SearchRankingOptions(ranker=\"weighted\", score_threshold=0.5)\n\n:param ranker: (Optional) Name of the ranking algorithm to use. Supported values:\n - \"weighted\": Weighted combination of vector and keyword scores\n - \"rrf\": Reciprocal Rank Fusion algorithm\n - \"neural\": Neural reranking model (requires model parameter)\n Note: For OpenAI API compatibility, any string value is accepted, but only the above values are supported.\n:param score_threshold: (Optional) Minimum relevance score threshold for results. Default: 0.0\n:param alpha: (Optional) Weight factor for weighted ranker (0-1).\n - 0.0 = keyword only\n - 0.5 = equal weight (default)\n - 1.0 = vector only\n Only used when ranker=\"weighted\" and weights is not provided.\n Falls back to VectorStoresConfig.chunk_retrieval_params.weighted_search_alpha if not provided.\n:param impact_factor: (Optional) Impact factor (k) for RRF algorithm.\n Lower values emphasize higher-ranked results. Default: 60.0 (optimal from research).\n Only used when ranker=\"rrf\".\n Falls back to VectorStoresConfig.chunk_retrieval_params.rrf_impact_factor if not provided.\n:param weights: (Optional) Dictionary of weights for combining different signal types.\n Keys can be \"vector\", \"keyword\", \"neural\". Values should sum to 1.0.\n Used when combining algorithm-based reranking with neural reranking.\n Example: {\"vector\": 0.3, \"keyword\": 0.3, \"neural\": 0.4}\n:param model: (Optional) Model identifier for neural reranker (e.g., \"transformers/Qwen/Qwen3-Reranker-0.6B\").\n Required when ranker=\"neural\" or when weights contains \"neural\".", + "description": "Options for ranking and filtering search results.\n\nThis class configures how search results are ranked and filtered. You can use algorithm-based\nrerankers (weighted, RRF) or neural rerankers. Defaults from VectorStoresConfig are\nused when parameters are not provided.\n\nExamples:\n # Weighted ranker with custom alpha\n SearchRankingOptions(ranker=\"weighted\", alpha=0.7)\n\n # RRF ranker with custom impact factor\n SearchRankingOptions(ranker=\"rrf\", impact_factor=50.0)\n\n # Use config defaults (just specify ranker type)\n SearchRankingOptions(ranker=\"weighted\") # Uses alpha from VectorStoresConfig\n\n # Score threshold filtering\n SearchRankingOptions(ranker=\"weighted\", score_threshold=0.5)\n\n:param ranker: (Optional) Name of the ranking algorithm to use. Supported values:\n - \"weighted\": Weighted combination of vector and keyword scores\n - \"rrf\": Reciprocal Rank Fusion algorithm\n - \"neural\": Neural reranking model (requires model parameter)\n Note: For OpenAI API compatibility, any string value is accepted, but only the above values are supported.\n:param score_threshold: (Optional) Minimum relevance score threshold for results. Default: 0.0\n:param alpha: (Optional) Weight factor for weighted ranker (0-1).\n - 0.0 = keyword only\n - 0.5 = equal weight (default)\n - 1.0 = vector only\n Only used when ranker=\"weighted\" and weights is not provided.\n Falls back to VectorStoresConfig.chunk_retrieval_params.weighted_search_alpha if not provided.\n:param impact_factor: (Optional) Impact factor (k) for RRF algorithm.\n Lower values emphasize higher-ranked results. Default: 60.0 (optimal from research).\n Only used when ranker=\"rrf\".\n Falls back to VectorStoresConfig.chunk_retrieval_params.rrf_impact_factor if not provided.\n:param weights: (Optional) Dictionary of weights for combining different signal types.\n Keys can be \"vector\", \"keyword\", \"neural\". Values should sum to 1.0.\n Used when combining algorithm-based reranking with neural reranking.\n Example: {\"vector\": 0.3, \"keyword\": 0.3, \"neural\": 0.4}\n:param model: (Optional) Model identifier for neural reranker\n (e.g., \"sentence-transformers/Qwen/Qwen3-Reranker-0.6B\"). Required when ranker=\"neural\" or when\n weights contains \"neural\".", "properties": { "ranker": { "type": "string", @@ -7009,6 +7214,152 @@ }, "title": "VectorStoresListResponse", "type": "object" + }, + "WebSearchActionFind": { + "description": "Web search action: searches for a pattern within a loaded page.", + "properties": { + "type": { + "const": "find_in_page", + "default": "find_in_page", + "title": "Type", + "type": "string" + }, + "url": { + "title": "Url", + "type": "string" + }, + "pattern": { + "title": "Pattern", + "type": "string" + } + }, + "required": [ + "url", + "pattern" + ], + "title": "WebSearchActionFind", + "type": "object" + }, + "WebSearchActionOpenPage": { + "description": "Web search action: opens a specific URL from search results.", + "properties": { + "type": { + "const": "open_page", + "default": "open_page", + "title": "Type", + "type": "string" + }, + "url": { + "type": "string", + "nullable": true, + "default": null, + "title": "Url" + } + }, + "title": "WebSearchActionOpenPage", + "type": "object" + }, + "WebSearchActionSearch": { + "description": "Web search action: performs a search query.", + "properties": { + "type": { + "const": "search", + "default": "search", + "title": "Type", + "type": "string" + }, + "query": { + "title": "Query", + "type": "string" + }, + "queries": { + "type": "array", + "nullable": true, + "default": null, + "title": "Queries" + }, + "sources": { + "type": "array", + "nullable": true, + "default": null, + "title": "Sources" + } + }, + "required": [ + "query" + ], + "title": "WebSearchActionSearch", + "type": "object" + }, + "WebSearchFilters": { + "description": "Domain filters for web search results.", + "properties": { + "allowed_domains": { + "type": "array", + "nullable": true, + "default": null, + "title": "Allowed Domains" + } + }, + "title": "WebSearchFilters", + "type": "object" + }, + "WebSearchSource": { + "description": "A source URL returned by a web search action.", + "properties": { + "type": { + "const": "url", + "default": "url", + "title": "Type", + "type": "string" + }, + "url": { + "title": "Url", + "type": "string" + } + }, + "required": [ + "url" + ], + "title": "WebSearchSource", + "type": "object" + }, + "WebSearchUserLocation": { + "description": "Approximate user location to refine web search results.", + "properties": { + "type": { + "const": "approximate", + "default": "approximate", + "title": "Type", + "type": "string" + }, + "city": { + "type": "string", + "nullable": true, + "default": null, + "title": "City" + }, + "country": { + "type": "string", + "nullable": true, + "default": null, + "title": "Country" + }, + "region": { + "type": "string", + "nullable": true, + "default": null, + "title": "Region" + }, + "timezone": { + "type": "string", + "nullable": true, + "default": null, + "title": "Timezone" + } + }, + "title": "WebSearchUserLocation", + "type": "object" } } }, diff --git a/docs/models/successful_responses.md b/docs/models/successful_responses.md index 649b0c527..099162736 100644 --- a/docs/models/successful_responses.md +++ b/docs/models/successful_responses.md @@ -696,6 +696,43 @@ Attributes: | object | string | Object type | +## GraniteGuardianConfig + + +Configuration for the Granite Guardian moderation guardrail. + + +| Field | Type | Description | +|-------|------|-------------| +| url | string | The model_id to use for the guard | +| api_key | string | API key for the inference | +| max_retries | integer | Maximun number of retires | +| timeout | integer | Request timeout in seconds | +| verify_ssl | | SSL certificate verification. Can be: + - True: Verify using system CA bundle (default, recommended) + - False: Disable verification (insecure, for dev only) + - str: Path to custom CA bundle file (for internal PKI) | +| risks | array | Risks to be considered while applying this guradrail | + + +## GraniteGuardianShieldConfiguration + + +Configuration for a named Granite Guardian guardrail shield. + +Attributes: + name: Unique, user-facing name identifying this shield instance. + provider_id: Discriminator identifying this as a granite-guardian shield. + config: Granite-guardian-specific configuration. + + +| Field | Type | Description | +|-------|------|-------------| +| name | string | Unique, user-facing name identifying this shield instance. | +| provider_id | string | Discriminator identifying this as a granite-guardian shield. | +| config | | Granite-guardian-specific configuration for this shield | + + ## HealthStatus @@ -1051,7 +1088,7 @@ Useful resources: - [OGX](https://ogx-ai.github.io/) - [Python OGX client](https://github.com/ogx-ai/ogx-client-python) - - [Build AI Applications with OGX](https://ogx-ai.github.io/) + - [Build AI Applications with OGX](https://ogx-ai.github.io/docs/building_applications) | Field | Type | Description | @@ -1385,14 +1422,13 @@ Function tool configuration for OpenAI response inputs. Web search tool configuration for OpenAI response inputs. -:param type: Web search tool type variant to use -:param search_context_size: (Optional) Size of search context, must be "low", "medium", or "high" - | Field | Type | Description | |-------|------|-------------| | type | | | | search_context_size | string | | +| filters | | | +| user_location | | | ## OpenAIResponseMCPApprovalRequest @@ -1595,16 +1631,13 @@ A summary of reasoning output from the model. Web search tool call output message for OpenAI responses. -:param id: Unique identifier for this tool call -:param status: Current status of the web search operation -:param type: Tool call type identifier, always "web_search_call" - | Field | Type | Description | |-------|------|-------------| | id | string | | | status | string | | | type | string | | +| action | | | ## OpenAIResponsePrompt @@ -1639,6 +1672,7 @@ Controls how much reasoning the model performs before generating a response. | Field | Type | Description | |-------|------|-------------| | effort | string | | +| generate_summary | string | Deprecated: use 'summary' instead. | | summary | string | Summary mode for reasoning output. One of 'auto', 'concise', or 'detailed'. | @@ -2362,6 +2396,44 @@ Configuration for a single retrieval strategy (inline or tool). | reranker | | Neural reranking of RAG chunks using cross-encoder. Only applicable to inline retrieval. | +## RiskDefinition + + +Definition for a custom risk category. + +Custom risks allow applications to add use-case-specific safety checks +beyond the standard harm, jailbreak, leetspeak, amnesia, and +history_politics checks. +Example: + liability_risk = RiskDefinition( + name="liability", + description="Content requesting legal, medical, or financial advice", + threshold=0.55, + points=["input"], + ) + pii_risk = RiskDefinition( + name="pii_request", + description="User is asking the AI to reveal personal information", + threshold=0.50, + points=["input", "tool"], + ) +Note: + To enable think mode (detailed reasoning) for a risk, add the risk name + to the `thinking_enabled` list in `ModerationConfig`. Do not set + `enable_thinking` directly - it is managed internally. + + +| Field | Type | Description | +|-------|------|-------------| +| name | string | Unique identifier for this risk (e.g., 'liability', 'competitor_mention') | +| description | string | Risk definition text passed to Granite Guardian as custom_criteria | +| threshold | number | Score threshold for flagging (lower = more sensitive) | +| enabled | boolean | Whether to run this check | +| enable_thinking | boolean | Internal field - set via ModerationConfig.thinking_enabled list, not directly. When True, Granite Guardian provides detailed reasoning before scoring. | +| points | array | Where this risk is evaluated: `input` (user message), `output` (model response), or `tool` (tool/MCP content). | +| violation_message | string | Message to be displayed when this risk is violated | + + ## RlsapiV1Configuration @@ -2566,8 +2638,9 @@ Examples: Keys can be "vector", "keyword", "neural". Values should sum to 1.0. Used when combining algorithm-based reranking with neural reranking. Example: {"vector": 0.3, "keyword": 0.3, "neural": 0.4} -:param model: (Optional) Model identifier for neural reranker (e.g., "transformers/Qwen/Qwen3-Reranker-0.6B"). - Required when ranker="neural" or when weights contains "neural". +:param model: (Optional) Model identifier for neural reranker + (e.g., "sentence-transformers/Qwen/Qwen3-Reranker-0.6B"). Required when ranker="neural" or when + weights contains "neural". | Field | Type | Description | @@ -3040,3 +3113,80 @@ Attributes: |-------|------|-------------| | data | array | List of vector stores | | object | string | Object type | + + +## WebSearchActionFind + + +Web search action: searches for a pattern within a loaded page. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| url | string | | +| pattern | string | | + + +## WebSearchActionOpenPage + + +Web search action: opens a specific URL from search results. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| url | string | | + + +## WebSearchActionSearch + + +Web search action: performs a search query. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| query | string | | +| queries | array | | +| sources | array | | + + +## WebSearchFilters + + +Domain filters for web search results. + + +| Field | Type | Description | +|-------|------|-------------| +| allowed_domains | array | | + + +## WebSearchSource + + +A source URL returned by a web search action. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| url | string | | + + +## WebSearchUserLocation + + +Approximate user location to refine web search results. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| city | string | | +| country | string | | +| region | string | | +| timezone | string | | diff --git a/src/README.md b/src/README.md index e5b29fee7..7ce0c9b1c 100644 --- a/src/README.md +++ b/src/README.md @@ -16,14 +16,14 @@ Constants used in business logic. Entry point to the Lightspeed Core Stack REST API service. -## [ogx_configuration.py](ogx_configuration.py) - -OGX configuration enrichment and synthesis. - ## [log.py](log.py) Log utilities. +## [ogx_configuration.py](ogx_configuration.py) + +OGX configuration enrichment and synthesis. + ## [version.py](version.py) Service version that is read by project manager tools. diff --git a/src/observability/README.md b/src/observability/README.md index 8dc8a8952..bfe0aadba 100644 --- a/src/observability/README.md +++ b/src/observability/README.md @@ -4,6 +4,10 @@ Observability module for telemetry and event collection. +## [profiling.py](profiling.py) + +Pyroscope CPU profiling initialization for Lightspeed Core Stack. + ## [responses_telemetry.py](responses_telemetry.py) Splunk telemetry helpers for the Responses API endpoint. diff --git a/src/pydantic_ai_lightspeed/llamastack/README.md b/src/pydantic_ai_lightspeed/llamastack/README.md new file mode 100644 index 000000000..473d0e008 --- /dev/null +++ b/src/pydantic_ai_lightspeed/llamastack/README.md @@ -0,0 +1,2 @@ +# List of source files stored in `src/pydantic_ai_lightspeed/llamastack` directory + diff --git a/src/utils/README.md b/src/utils/README.md index 0efedb069..4763ed133 100644 --- a/src/utils/README.md +++ b/src/utils/README.md @@ -48,10 +48,6 @@ Input sanitization to detect and block obfuscated prompt injection attempts. Function to transform a JSON Schema-like dictionary into an OpenAPI-compatible schema. -## [ogx_version.py](ogx_version.py) - -Check if the OGX version is supported by the LCS. - ## [markdown_repair.py](markdown_repair.py) Utilities for repairing truncated markdown content. @@ -76,9 +72,13 @@ Utilities for discovering tools from remote MCP servers without OGX. Helpers for normalizing OGX ``models.list()`` union responses. -## [openapi_schema_dumper.py](openapi_schema_dumper.py) +## [ogx_serialization.py](ogx_serialization.py) -Utility function to dump schema with list of models into OpenAPI-compatible JSON format. +Serialization helpers for ``ogx_client`` models. + +## [ogx_version.py](ogx_version.py) + +Check if the OGX version is supported by the LCS. ## [otel_tracing.py](otel_tracing.py) diff --git a/src/utils/dumpers/README.md b/src/utils/dumpers/README.md index f3e7acda3..dc107130b 100644 --- a/src/utils/dumpers/README.md +++ b/src/utils/dumpers/README.md @@ -8,3 +8,7 @@ Function to dump the configuration schema into OpenAPI-compatible format. Function to dump the schema of all data models into OpenAPI-compatible format. +## [openapi_schema_dumper.py](openapi_schema_dumper.py) + +Utility function to dump schema with list of models into OpenAPI-compatible JSON format. + diff --git a/tests/unit/README.md b/tests/unit/README.md index 691a2585e..c98915e6b 100644 --- a/tests/unit/README.md +++ b/tests/unit/README.md @@ -24,6 +24,10 @@ Unit tests for the degraded mode tracker. Unit tests for functions defined in src/lightspeed_stack.py. +## [test_log.py](test_log.py) + +Unit tests for functions defined in src/log.py. + ## [test_ogx_configuration.py](test_ogx_configuration.py) Unit tests for src/ogx_configuration.py. @@ -32,7 +36,3 @@ Unit tests for src/ogx_configuration.py. Unit tests for unified-mode OGX configuration synthesis (LCORE-2336). -## [test_log.py](test_log.py) - -Unit tests for functions defined in src/log.py. - diff --git a/tests/unit/models/config/README.md b/tests/unit/models/config/README.md index c60c41ee9..c251ab0ec 100644 --- a/tests/unit/models/config/README.md +++ b/tests/unit/models/config/README.md @@ -56,10 +56,6 @@ Unit tests for InferenceConfiguration model. Unit tests for JwtRoleRule model. -## [test_ogx_configuration.py](test_ogx_configuration.py) - -Unit tests for OgxConfiguration model. - ## [test_model_context_protocol_server.py](test_model_context_protocol_server.py) Unit tests for ModelContextProtocolServer model. @@ -68,6 +64,10 @@ Unit tests for ModelContextProtocolServer model. Unit tests for ObservabilityConfiguration model. +## [test_ogx_configuration.py](test_ogx_configuration.py) + +Unit tests for OgxConfiguration model. + ## [test_postgresql_database_configuration.py](test_postgresql_database_configuration.py) Unit tests for PostgreSQLDatabaseConfiguration model. diff --git a/tests/unit/pydantic_ai_lightspeed/llamastack/README.md b/tests/unit/pydantic_ai_lightspeed/llamastack/README.md new file mode 100644 index 000000000..7944b07e8 --- /dev/null +++ b/tests/unit/pydantic_ai_lightspeed/llamastack/README.md @@ -0,0 +1,2 @@ +# List of source files stored in `tests/unit/pydantic_ai_lightspeed/llamastack` directory + diff --git a/tests/unit/utils/README.md b/tests/unit/utils/README.md index 34683e80d..c5c81aaf3 100644 --- a/tests/unit/utils/README.md +++ b/tests/unit/utils/README.md @@ -44,10 +44,6 @@ Unit tests for utils/input_sanitization.py. Unit tests for utils/json_schema_updater module. -## [test_ogx_version.py](test_ogx_version.py) - -Unit tests for utility function to check OGX version. - ## [test_markdown_repair.py](test_markdown_repair.py) Unit tests for markdown repair utilities. @@ -68,6 +64,14 @@ Unit tests for MCP tool discovery utilities. Unit tests for utils/model_list.py helpers. +## [test_ogx_serialization.py](test_ogx_serialization.py) + +Unit tests for ogx_client serialization helpers. + +## [test_ogx_version.py](test_ogx_version.py) + +Unit tests for utility function to check OGX version. + ## [test_otel_tracing.py](test_otel_tracing.py) Unit tests for utils/otel_tracing.py functions. From 1e281e9b6e609b0696bec16cd2ca32a265d4ebc3 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Wed, 9 Sep 2026 10:25:22 +0200 Subject: [PATCH 036/120] LCORE-3300: Final fix in vulnerability report script --- scripts/vulnerability_report.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/scripts/vulnerability_report.py b/scripts/vulnerability_report.py index 28364695b..e49424239 100644 --- a/scripts/vulnerability_report.py +++ b/scripts/vulnerability_report.py @@ -42,7 +42,6 @@ import matplotlib.pyplot as plt from dateutil import parser -from matplotlib.figure import Figure type DependabotAlert = dict[str, Any] type DependabotAlerts = list[DependabotAlert] @@ -549,12 +548,12 @@ def generate_new_cve_dates_graph( svg_output (bool): Whether to save the graph as SVG. png_output (bool): Whether to save the graph as PNG. """ - fig, ax = plt.subplots() - D = stat["dates"] - dates, counts = zip(*sorted(D.items(), key=lambda x: x[0])) + _, ax = plt.subplots() + data = stat["dates"] + dates, counts = zip(*sorted(data.items(), key=lambda x: x[0])) ax.plot(dates, counts) ax.set_title("New CVEs timeline") - save_graph(fig, prefix, "timeline", svg_output, png_output) + save_graph(prefix, "timeline", svg_output, png_output) def generate_graphs( From b28e62bf134664ded5fb1a23d94d720e39f584b2 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Wed, 9 Sep 2026 10:27:38 +0200 Subject: [PATCH 037/120] LCORE-3580: Updated dependencies --- uv.lock | 122 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/uv.lock b/uv.lock index 63da73e72..ea78812e1 100644 --- a/uv.lock +++ b/uv.lock @@ -218,46 +218,46 @@ wheels = [ [[package]] name = "ast-serialize" -version = "0.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/59/6d/c6ab91f72f4862d63e446e10248d8f52e8b4c4b7579bb937ddf942a5961e/ast_serialize-0.10.0.tar.gz", hash = "sha256:f47a26cc7d2605fb645b6e7f6c21cf4fb8d7833d00ea8b48e26f057b14eccd01", size = 952608, upload-time = "2026-09-07T10:22:47.595Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/bd/cf2deab2ceb79f4476a30479a2756bb64b63810b7f10bfbd3ce1503d4059/ast_serialize-0.10.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:cbcc3542259ae08153e634d50dc220723415fb24510856523052b5eeb10b4950", size = 1229414, upload-time = "2026-09-07T10:21:49.554Z" }, - { url = "https://files.pythonhosted.org/packages/ee/cb/ec8e84d3ab8073e8536823f7a3cba46e4992d9c8d904481e18509eeea3ff/ast_serialize-0.10.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:2251086be53a375d256faefaedffe5ccd99187be614788f8279b41dae1c5fdee", size = 1210297, upload-time = "2026-09-07T10:21:50.996Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e7/6a1d216160da29566a6125c34e90092a5f9d1444e24e955741113abdf033/ast_serialize-0.10.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1358ef5d98f9dc96468d171317d78d746f9ef8ca45577298512a737ba9d6514", size = 1276064, upload-time = "2026-09-07T10:21:52.452Z" }, - { url = "https://files.pythonhosted.org/packages/96/87/1f1052a0dd00dfe15c129a7073f7b1b5bb632b5507baff27190dd255e0a9/ast_serialize-0.10.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:59ef8acc0ba4315024e5000e5fa8ea656a02e375ebb5ce020f3582d308d6153d", size = 1280982, upload-time = "2026-09-07T10:21:53.992Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8f/9f4578b668b60436249dfa1bc6612283d3cbb665bc6cb2c1e23934d708d8/ast_serialize-0.10.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c9715c4379b33740c94819e55afe59e28d6d3f30f69b903b4fcb2ecb5335531", size = 1552458, upload-time = "2026-09-07T10:21:55.485Z" }, - { url = "https://files.pythonhosted.org/packages/e6/cc/395d96c55e5d91a87fcfa5f15c7d11834cd150d491ce4849b2290334b8e9/ast_serialize-0.10.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26e6dc5d1f7aa642d9e94b98e4d3e1c81dc6b595f01619658f7e9268379826c0", size = 1298414, upload-time = "2026-09-07T10:21:57.01Z" }, - { url = "https://files.pythonhosted.org/packages/7a/8d/473c8fb551af1de55abcc36d591ac18d4175ba7fc15f41d62ad9b30d7385/ast_serialize-0.10.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760dce78c6c4d4df30d3df9b80be1c521e0ee1d275ae54906e9a95b150692567", size = 1296613, upload-time = "2026-09-07T10:21:58.591Z" }, - { url = "https://files.pythonhosted.org/packages/6f/57/31044aa6fa739634ada7fff321db08a813157663b18629851820fca455a6/ast_serialize-0.10.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:40a7f487e2f5523518270600e0b7f2c61beac3106da142d911c3386ed7abbcfa", size = 1304321, upload-time = "2026-09-07T10:22:00.12Z" }, - { url = "https://files.pythonhosted.org/packages/10/87/c5d7c0c627b991f46fc308a41c0e92b792322440f84e43739b11fb4a1a64/ast_serialize-0.10.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1c252b746da0ad1883b82502eeff859221eef1d66d3f95dcb2373760a34d440", size = 1350352, upload-time = "2026-09-07T10:22:01.605Z" }, - { url = "https://files.pythonhosted.org/packages/62/ec/d80abe681b7d96b4ea88cf69d559d55a41b1a36c907b579d774caff2527a/ast_serialize-0.10.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:719c7a121cc3022d78b7f4f7a9fd159586e521531c0e495a245375d2c4695d4e", size = 1454522, upload-time = "2026-09-07T10:22:03.149Z" }, - { url = "https://files.pythonhosted.org/packages/8d/7a/d4cc3aa236b9fdce16a394bfd7c12072d9ac30e83f9319aaa14305cbaab9/ast_serialize-0.10.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:d7621157ac99ae00957196e7094fe279e7547d8b505046a9d5509c4fc740e9fc", size = 1555150, upload-time = "2026-09-07T10:22:04.722Z" }, - { url = "https://files.pythonhosted.org/packages/d2/a6/710d1aef678650c75edcb4cdab79998ec63aae54710a438b9fd66c3a616e/ast_serialize-0.10.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:82087a71fe39925ab0068ef2f25444a1c87d1e5f1901f32dcf82f6eddddeac6a", size = 1551843, upload-time = "2026-09-07T10:22:06.182Z" }, - { url = "https://files.pythonhosted.org/packages/e9/23/2a9c35721a82a506bae980dbdfeaa7b296bf79d818c731b3aaa0ee53c5a7/ast_serialize-0.10.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:8aac958cf0c0b2595a0487c7b5d2ecac88fa5c0221b83b1420ac4bf472f84cb7", size = 1686064, upload-time = "2026-09-07T10:22:07.712Z" }, - { url = "https://files.pythonhosted.org/packages/24/36/f3fd4b6f2c1e4d5eca149e4bfdaa6d93fe77851b21562d14f22a452c71ee/ast_serialize-0.10.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:a53a4d528171b8b709b4e7d89654532189bff53947f24ac4db9c465bb10e7cd2", size = 1478662, upload-time = "2026-09-07T10:22:09.218Z" }, - { url = "https://files.pythonhosted.org/packages/de/65/5135ac9c7e305bd3e6234ef0fbf50bbac2ac00a35076bc167936c6d6d166/ast_serialize-0.10.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:93ae740c6d6f5641573122a7dcffa4baa7ef9144109c54584baad5bf827ad388", size = 1495057, upload-time = "2026-09-07T10:22:10.742Z" }, - { url = "https://files.pythonhosted.org/packages/5a/dd/7dd0e80551dc66fc56197b5439d76fb6173ccf9975df6bf1399e2231069c/ast_serialize-0.10.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:4d10d8fb4b82d5bae455ce9f606920d8dcd563b812b43dcd8d6f9b11089cfb23", size = 1114912, upload-time = "2026-09-07T10:22:12.675Z" }, - { url = "https://files.pythonhosted.org/packages/72/83/a84661b89065fe4986d183abdf0bce5f1a24bea26bbe74054345758357c1/ast_serialize-0.10.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:5917460c0f8322755872db1792788fcaa3835d1fb6a3f129c65ace700c7e4663", size = 1152503, upload-time = "2026-09-07T10:22:14.159Z" }, - { url = "https://files.pythonhosted.org/packages/f6/c0/95bc047bc830f3223d42111ca2980a4328cd09d597773f04f2c689c3330e/ast_serialize-0.10.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:8efb53cf1c439516a563664e014ba9b6b4fd86b4869c180954edce3954a51870", size = 1122527, upload-time = "2026-09-07T10:22:15.656Z" }, - { url = "https://files.pythonhosted.org/packages/70/85/4606f3398e6776c74d44f8387855775c3cf31614b664cdb0e147df752933/ast_serialize-0.10.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:382cb721e2624dd1c2f4f7afd2a9b500930b057d2e24e1078cf6999c5fad1b31", size = 1236237, upload-time = "2026-09-07T10:22:18.767Z" }, - { url = "https://files.pythonhosted.org/packages/09/7b/520b33339f3d5ef9407fb17a00dac0849318158fa0c9853cfe8cc6b7a48d/ast_serialize-0.10.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:56a28b7567ba17f602a17d5603133a1292d307e957b3b5c3f66538e87548aed3", size = 1223110, upload-time = "2026-09-07T10:22:20.243Z" }, - { url = "https://files.pythonhosted.org/packages/a0/af/3c30941ed368ddb54e6ec0edfdd98dc7fdc679c6d4f478f8545c0b8f080a/ast_serialize-0.10.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8919e79a4525cdac63d51cd117c57f324936c902c7ecd0fd09d02aa88323b74d", size = 1285870, upload-time = "2026-09-07T10:22:22.019Z" }, - { url = "https://files.pythonhosted.org/packages/56/81/f0c0bf569388d319aab22c8a7f43747450e26c5ae6a6d9521766157f70f4/ast_serialize-0.10.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1109077d53b377d022d799bc4b171c32c3a711a716248a918d3e07875b5513cd", size = 1290793, upload-time = "2026-09-07T10:22:23.562Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a1/515a76479ecfc77abe88d7c9954bc3a04f07b30fa33ec836eea80be07fd4/ast_serialize-0.10.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d17c50e1f6e8d950d79c66a18b863dee16cea3106f597203883131dd9c6ca4f", size = 1561250, upload-time = "2026-09-07T10:22:25.057Z" }, - { url = "https://files.pythonhosted.org/packages/4f/ba/702af61eb7a6803bf4cd30c09041ab4212e63d553ed9dc2793c95ab340b4/ast_serialize-0.10.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf2c6cc5a62839869317523e0f74737db0d1759d72fb62cffe57a40e040d6a55", size = 1305696, upload-time = "2026-09-07T10:22:26.612Z" }, - { url = "https://files.pythonhosted.org/packages/31/ce/e6e0a311c16f84f13528f1cb32dec1ce68102711070c10c0c688a6c3cdf5/ast_serialize-0.10.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb1b20121a62da5937b0c11c8f31667be0102342bf55459bb44a10d8549f8f3f", size = 1302712, upload-time = "2026-09-07T10:22:28.088Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a2/7fd715228a35fec1c587c8156eaabf37b6df98fea48c90eec8f1af362e20/ast_serialize-0.10.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:6e45f7d7a663c28ed44d52b4069e32335edb7a0653a908a43bef2801d8d7c344", size = 1313977, upload-time = "2026-09-07T10:22:29.642Z" }, - { url = "https://files.pythonhosted.org/packages/1b/ff/e1c6a0aefdc80695c2dda0c558d58f90f55cb7bb791545865cd7d4ec39d7/ast_serialize-0.10.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c75ed7d94660e3ed4fd3c35672ab16c248c0be51d6b845de3edebbb6cf4526c", size = 1360341, upload-time = "2026-09-07T10:22:31.289Z" }, - { url = "https://files.pythonhosted.org/packages/08/ef/ab341936f65b663e4909dd5d8772d7c13dd7a39616cf18fd6231197b8177/ast_serialize-0.10.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:45cfbde7e43a56a386b47f13b415d1fa4421b0a5e07ae0c84a7d883ac9aca4b2", size = 1462406, upload-time = "2026-09-07T10:22:32.823Z" }, - { url = "https://files.pythonhosted.org/packages/74/08/1cef69ee4228cd82b9da6b5e29e7fcdd30a6b8b3d6332815781e09666b62/ast_serialize-0.10.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:b3a52baa4c457d17a5729aa9bd698da8e61731f1670b4fe33923da54daa480e3", size = 1566792, upload-time = "2026-09-07T10:22:34.381Z" }, - { url = "https://files.pythonhosted.org/packages/d2/01/a3d773d0fe1536485069953d54bece7d8a95aa688e64f5cd692f68949051/ast_serialize-0.10.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:c07cb3f354c234cfacf81da95194b085186d7f4c1f7ad06c7f04fb49e320ef6d", size = 1560873, upload-time = "2026-09-07T10:22:35.844Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ba/7fdda55197f06b2a1613d2ed7e647035b40b26b7aff62ac78e6728128ce9/ast_serialize-0.10.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:8c91b2bc42a12252be53966a1f1b14cc5c843dfca82bee3bce38f2e96b327aa6", size = 1693061, upload-time = "2026-09-07T10:22:37.355Z" }, - { url = "https://files.pythonhosted.org/packages/3a/29/630a7cafd2780651ed73d468518be9d08a6cc3e7c30fdc88c0b2b90c1c5a/ast_serialize-0.10.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:cb698ad6625a55d5d618c9d887423fd04d65dc5ad0f3ae5a8cbd96c46cea2438", size = 1487731, upload-time = "2026-09-07T10:22:39.403Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c3/84fc44b110ee10fb410d49f7f28686c4d23637161b6cb08775c1af8a4507/ast_serialize-0.10.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4496c0d20111a243525cb074a3e9a9313bbd7d3fb1d40ddee2ade9ef680b80c6", size = 1501731, upload-time = "2026-09-07T10:22:40.958Z" }, - { url = "https://files.pythonhosted.org/packages/59/9d/eedfd5a8cbeffd7172281e14d7a7e7640ed3997b3a09382c7756e39a2bbd/ast_serialize-0.10.0-cp39-abi3-win32.whl", hash = "sha256:ca5ff9030b426b245cf7897137872cdb6ebe43b2e4c5e04d5812ed24955a91ba", size = 1121201, upload-time = "2026-09-07T10:22:42.629Z" }, - { url = "https://files.pythonhosted.org/packages/a9/83/2db45120bd0ae66cfcd5e235d4dd29aec564e56440ebb17ec83428bb2685/ast_serialize-0.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:f1a8508054137a1cc67a64915ba588e75f03f546a9b16424aaf0cc9854b0a2a2", size = 1158429, upload-time = "2026-09-07T10:22:44.546Z" }, - { url = "https://files.pythonhosted.org/packages/53/f4/3850ee0050b68f3bd31c06a70cb065f7b129123e5795ff8a0ef642db71da/ast_serialize-0.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:354d12c7463266f2ca655af6f0333ee91ea98c875ffc5f47f10f03c7fe8a0667", size = 1130605, upload-time = "2026-09-07T10:22:46.082Z" }, +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/c2/feb42ca5bf2335aa16868bc53ec83f114923564763658d6a63e4c4c6ccde/ast_serialize-0.11.0.tar.gz", hash = "sha256:8b4b9862436eaf1442d6a16b7e138d4ff6ae558bdb6fde1c4cd87735093d5744", size = 953724, upload-time = "2026-09-08T14:58:39.804Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e1/cf820beb541ab4177a3566be92478e2f5cadfc1306c0cc5300c174dde1a3/ast_serialize-0.11.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:75d12e18c8d6691ceda09f514de2effcac628b217c0b3ea17851d217bfdbdab8", size = 1234341, upload-time = "2026-09-08T14:57:47.197Z" }, + { url = "https://files.pythonhosted.org/packages/89/01/8859ba83facbb1d57720eec1262d01547df3f9269d527eba36b785137a29/ast_serialize-0.11.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:ee8928ae9305f921484239a97030146dbb674cc10327fdf84124c4a20f73382e", size = 1212409, upload-time = "2026-09-08T14:57:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/91/b5/2938098537ea94c204f85cc34acc41fec8a7bd3986b8d617f84cf25e802c/ast_serialize-0.11.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c345fb4e035ca5db948b781d894f5d98d8a271497ce0fee319f9375d5d358c9b", size = 1280678, upload-time = "2026-09-08T14:57:49.787Z" }, + { url = "https://files.pythonhosted.org/packages/41/7b/4d36b08ecad936af6f09dac75ed30198ec9f6c615ffe9e478ed6dd1450bc/ast_serialize-0.11.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7523fce1258c810c277799557771e35a851e5c1f132d0a68ca4551675f868b7", size = 1286678, upload-time = "2026-09-08T14:57:51.106Z" }, + { url = "https://files.pythonhosted.org/packages/95/ea/2fc504ddb5ed27c78f9de75c1835c365973d16d8d7f94a10ddf47f903c42/ast_serialize-0.11.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d861d8232f6c1661b805f22c96c3923a76c8feed1a4e504cecc0eaa3cd4077e", size = 1556836, upload-time = "2026-09-08T14:57:52.564Z" }, + { url = "https://files.pythonhosted.org/packages/c0/56/36bd4b5a73178ffede18e20020a73e153809b4d4d44ba8b5cfa2f80be6d6/ast_serialize-0.11.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:93206e8059d23952475099967174b7a3f573ca7dc19bdcaa59dbfbf86d76e52b", size = 1301292, upload-time = "2026-09-08T14:57:54.052Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b8/88fb5bb26cf5614174b74172118461c17511dbbd8f827f0bde4da7280da2/ast_serialize-0.11.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18e28f523669d07547795aac3d9a5cf9e7c8d6ddf3dd48fe2090009ab35d0f69", size = 1299691, upload-time = "2026-09-08T14:57:55.67Z" }, + { url = "https://files.pythonhosted.org/packages/81/45/382d0e3426efefda064ef24036c2ca5905f4e8efeadd87ef73df55e010da/ast_serialize-0.11.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:cd6b802249a22d0f44cee22452910a653493e4b974a8dbe401f6b85cb4d74219", size = 1308581, upload-time = "2026-09-08T14:57:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/43/d0/71e43cd333fe46c984233819891a22648d4c7c1c3a7ae98d6e3945386c81/ast_serialize-0.11.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3f552edbf9e7e0fe69b1eda569cf775c2279e5f0b839727d63c39a73adaef966", size = 1356809, upload-time = "2026-09-08T14:57:58.494Z" }, + { url = "https://files.pythonhosted.org/packages/22/bc/b916b5a3ce58a2b540611dd7ece93e4015c30328f3d6459f79fc252d0825/ast_serialize-0.11.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:0ecc5e1282b8ce7c73fc204bde4eb3bad500784e1f0e14f0b9ea624124095821", size = 1458221, upload-time = "2026-09-08T14:57:59.798Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/3173921ff69ad214bc54172c572719f792faf118bca3485f412bbf26716f/ast_serialize-0.11.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:2110f04937ac932c03839af266a57a8b29fd97b0789d500fa92782bdad70f475", size = 1562074, upload-time = "2026-09-08T14:58:01.096Z" }, + { url = "https://files.pythonhosted.org/packages/4f/69/49cbb574085b5d08f9384d1066f5041fea42e0d79d1f4f39c95cafb54897/ast_serialize-0.11.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:2fc6fcdbf95a358f3be9af5ee8ccad40e0ef61ed6b84b942003e5bd983ac4cc9", size = 1556027, upload-time = "2026-09-08T14:58:02.581Z" }, + { url = "https://files.pythonhosted.org/packages/cb/59/13a559d9a28ef6ea38904ee4e766b49860ba046a12842d7620668f3c57b7/ast_serialize-0.11.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:6a5879e45a85c0cce453c1302250a9d644bab59c2d615e6720da03c2d1baffe3", size = 1689378, upload-time = "2026-09-08T14:58:03.889Z" }, + { url = "https://files.pythonhosted.org/packages/da/20/77d92e56456dc047c673fd9d1d2adfb64d767479ad4a4e1d7089e95030c0/ast_serialize-0.11.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:6cf3edf38d808e3f26094473f593feb3c05d68d4c53b366c9c3a1c3284438f71", size = 1482258, upload-time = "2026-09-08T14:58:05.219Z" }, + { url = "https://files.pythonhosted.org/packages/8c/41/eca650f9ed98f3c84f8d498b8f1268388090b71606afb0dede4eb977b449/ast_serialize-0.11.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:c912d84a7eed20aa51ce864a37e60c275a3c4f972056aab20247ec8d01b8457e", size = 1499691, upload-time = "2026-09-08T14:58:06.772Z" }, + { url = "https://files.pythonhosted.org/packages/9d/79/94e77924e793ca881d240f71fd5cf0929f05bcaf8e293b134ef8254643ad/ast_serialize-0.11.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:f504a2c787d22822fcfa4297bbb014cef1dc06962a30d2710c8c1cd5cf8326ec", size = 1118536, upload-time = "2026-09-08T14:58:08.478Z" }, + { url = "https://files.pythonhosted.org/packages/be/f1/024c32d13b5c62df0bbc4bb37eea7b1b49068d77bacfbb1e82a83fb2260e/ast_serialize-0.11.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:d0ee62b0149144785e2ea158cade30a41d90446cbaeae5745cb996adcee99c68", size = 1155492, upload-time = "2026-09-08T14:58:10.016Z" }, + { url = "https://files.pythonhosted.org/packages/cd/14/671534a8129604577e1c1ada3f21b32982c46d18c21e4e3511c2da8011bf/ast_serialize-0.11.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:2468e68d7f5b6d80fb619f614734f46ab8c2d474437bcba16cf3f63b5e1b3f79", size = 1126547, upload-time = "2026-09-08T14:58:11.361Z" }, + { url = "https://files.pythonhosted.org/packages/a5/52/0e5e3afe70f9bded7a4643734c685ddd2e33932e3fed9b29836fc87238fc/ast_serialize-0.11.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d2e3fb7476f2ccc2c2e40155f8e04e6110be98b6f51e52cc5476f3be5b1a1956", size = 1238152, upload-time = "2026-09-08T14:58:14.081Z" }, + { url = "https://files.pythonhosted.org/packages/6a/91/0fc54081b97feb2fe440aa1b84f32920608fb5b93a25b42cee0b55a4900a/ast_serialize-0.11.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:fc0a2fc3335a72a750c84e2a8cf113e4265edc930ac1b64b5f52fb7685070e3a", size = 1227156, upload-time = "2026-09-08T14:58:15.406Z" }, + { url = "https://files.pythonhosted.org/packages/96/8b/fe9a2da2839f2b94db49ebbd3404aefc43daaa91d250116c1a8817b908e9/ast_serialize-0.11.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26803ffd2daac676b4acf6683d527af091ed2fbf1c7eca861216702c3296d408", size = 1289363, upload-time = "2026-09-08T14:58:16.804Z" }, + { url = "https://files.pythonhosted.org/packages/cb/41/b4d53de75b60e5b4bdffab76acfd4d25cde840080a1f5741f37f8d6dad42/ast_serialize-0.11.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c394f64c4a7aba67173dda8b70087b37c82dc899265dca160a9ccd0d2c9e1ff5", size = 1293293, upload-time = "2026-09-08T14:58:18.174Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d6/337593438d847e78c93f26fc2eb670680759fe9c19e0975a82720e850b0c/ast_serialize-0.11.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce307fe34458a9fb87c660730796e55f977acf5c4e9c645a806faf3436d402a9", size = 1564305, upload-time = "2026-09-08T14:58:19.481Z" }, + { url = "https://files.pythonhosted.org/packages/cd/51/40aec6d8c3afc2e6cba24fe496ae43b43dfdcb3f84de96e0746bc4f83cd6/ast_serialize-0.11.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82f67de691ae29aa44afc6fff5d7c77f93bd5218954757a9453ebdbe924cfb12", size = 1307996, upload-time = "2026-09-08T14:58:20.91Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/53e67994ed4d19972f958cc69759caf71c28d3f7d094152affc1d47006c7/ast_serialize-0.11.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2605d538e17e9569643b5c0722da8d39b1d758391e2a1902f44abfe42b8f7f78", size = 1309997, upload-time = "2026-09-08T14:58:22.463Z" }, + { url = "https://files.pythonhosted.org/packages/b9/50/d3555436b54feb7a11affe579113f6fcacd848be2a505592162e5930fcca/ast_serialize-0.11.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:8dcae98b677ca0ed9988d09b83394db65385ca6f45b0ee8892e68c90e5d8df74", size = 1318566, upload-time = "2026-09-08T14:58:24.106Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f6/e45e90b6c7798d9c042c3e8c12357edd16131f7ea779228d79caf0bb858c/ast_serialize-0.11.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a2ed2669adf5e92f4d0cfd70a72c8f949c66309657eabb54a2c7a0ba2577f8d", size = 1364618, upload-time = "2026-09-08T14:58:25.582Z" }, + { url = "https://files.pythonhosted.org/packages/71/90/2f8705717d6816ed77d2378f118b9e7664dc73a2d34bd2a668dc0b8e7bfd/ast_serialize-0.11.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:60d3fd70362b539318e1149ca09df0d0f9ff03e7ea72b117453003a9d8598ab7", size = 1466509, upload-time = "2026-09-08T14:58:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/80/72/fde49aae39cfddf9736df4911e12f66120c99ff6e002690e50cd24697417/ast_serialize-0.11.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1241b3963a07bd474f0a5d53f054bbbda278c805efd1d7b4ce41d0d3c5becc1e", size = 1571073, upload-time = "2026-09-08T14:58:28.444Z" }, + { url = "https://files.pythonhosted.org/packages/25/87/f5a9d0ce56d417ea831d4627358884af3695db682f7f7bb167751e79f891/ast_serialize-0.11.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:a2fbc5cbff379354d6e07296f0f953955543f60134ffd91fb8f14fc0b4f1aeaa", size = 1566781, upload-time = "2026-09-08T14:58:29.713Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b0/af07c6974ef839dd2d4f2a2123e106a9a484a42786855b7a59281f8fe1f3/ast_serialize-0.11.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc0c793063fe07e52244c66ceb2b2c5eecaa826dbb250fdeb7f0502e5697782", size = 1696239, upload-time = "2026-09-08T14:58:30.948Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/c8b17c359650809ee00badf2179d90222f09e889662bead41a4695b0c00c/ast_serialize-0.11.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:d475173e59fec5dd1bad84d8351c7f5230119b1d5c68bad93264657589a93043", size = 1491560, upload-time = "2026-09-08T14:58:32.576Z" }, + { url = "https://files.pythonhosted.org/packages/04/71/29ca37b4bc78d924e54d2a5d4daa6d5a142daf0b7f614996b651225079e3/ast_serialize-0.11.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:26bd819c06ee10df68dbcf2fb12c8fd2249bcadffe25ba7011f0686cbfd69b94", size = 1507743, upload-time = "2026-09-08T14:58:33.827Z" }, + { url = "https://files.pythonhosted.org/packages/54/5b/8f3381e22b86e3bdb74898ea4f7a15b54cca308a63e9e02753f99d6dd4fe/ast_serialize-0.11.0-cp39-abi3-win32.whl", hash = "sha256:4c4c2df749d1dc6bba9a9e8cc4b5c3dfc39a351fbfe4ce22ca49c2d488c7a7ce", size = 1124421, upload-time = "2026-09-08T14:58:35.332Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b0/1ddfbc7aec32df2ed87ad830ee55854ddeceb2e8a8fcfae9b6ed57be0b4b/ast_serialize-0.11.0-cp39-abi3-win_amd64.whl", hash = "sha256:dd3c69e27b1be3172880bce2867c359106c28c5909f1ca6154bb7bf89847c419", size = 1162052, upload-time = "2026-09-08T14:58:36.674Z" }, + { url = "https://files.pythonhosted.org/packages/34/0b/32f3c8162cb5b33f24bea94503dbdd6b55aee72d367d23a55f1338f3e1b7/ast_serialize-0.11.0-cp39-abi3-win_arm64.whl", hash = "sha256:eb22d9300e7a064fa8c45e2c1e568a4e36c4c91487ea0da5e7f589905365c866", size = 1134422, upload-time = "2026-09-08T14:58:38.267Z" }, ] [[package]] @@ -429,30 +429,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.43.89" +version = "1.43.90" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/26/48b3da85526a72a02df55e564481fc348e93699c15f0f502681b12ac2c8a/boto3-1.43.89.tar.gz", hash = "sha256:c28abbe472e9b7cad08807356311aeec51bde5218c18489da827045d2267bfd9", size = 112702, upload-time = "2026-09-04T19:24:57.143Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/0b/53f833efea7331aab106a65f1156e0035beed9bacbfcb309b128204468d4/boto3-1.43.90.tar.gz", hash = "sha256:4b669742d5b45b8fd20ca50ac414a4e4cf995ebb8f280d21be28676e71c97594", size = 112689, upload-time = "2026-09-08T19:22:30.875Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/12/e1b5cb4a00a9bfd72cf2d3f982c5826757aacdfc90aa4bd61902dcc94856/boto3-1.43.89-py3-none-any.whl", hash = "sha256:fe4190afe63eb562b6ba6a3911cf4427473b35fa047adde093bf696d3ae09fc0", size = 140028, upload-time = "2026-09-04T19:24:55.929Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c3/1d5c5ea7f599e2b80d122beeb2a9c1a8daff78325ea2391521e5fd5af1fd/boto3-1.43.90-py3-none-any.whl", hash = "sha256:aaaa1216d65ddb3dcf86bf9d93cfa436af05cbcd1e1f8b2847f2b83116012186", size = 140026, upload-time = "2026-09-08T19:22:28.555Z" }, ] [[package]] name = "botocore" -version = "1.43.89" +version = "1.43.90" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/06/f63fb1befdf77af18539fb24ea01f2da0f13965ed5de091061708ac96416/botocore-1.43.89.tar.gz", hash = "sha256:f0574942970742657b0e0716cf08c2dfe6bef8e6de5fbb7081c3424e262b4cca", size = 16074206, upload-time = "2026-09-04T19:24:52.464Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/3e/29872261075d878581a31e9e98c512c0b85322a36e6d6784b9f0694a918d/botocore-1.43.90.tar.gz", hash = "sha256:a139ed601e8b8fb1d730022355fe2b284b8c15cfe9ac0f100254a35d2273e1d3", size = 16081908, upload-time = "2026-09-08T19:22:25.645Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/9d/96f9dee6d12eedf1c2b4264eefd59c7ac8cac10daadb9a7bfccc9ee881c6/botocore-1.43.89-py3-none-any.whl", hash = "sha256:d7211220c815427fe71225acc6909e4ab5dfab3b03770e72fd16cf9eb86b3d1a", size = 15768272, upload-time = "2026-09-04T19:24:49.769Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/65cbd0d65553c3e51d27159de4c74308ea008c652b05ed3f830f32667efb/botocore-1.43.90-py3-none-any.whl", hash = "sha256:65f3394ef07314e45c92a90120988531d651136390d464a94938a92f665893f7", size = 15775313, upload-time = "2026-09-08T19:22:22.713Z" }, ] [[package]] @@ -992,11 +992,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.32.5" +version = "3.32.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/a0/50c2c0ce5e74d7721bbb1b19a26ebd339aac5878553a6e35308c2f31f935/filelock-3.32.5.tar.gz", hash = "sha256:f6a6a28f743f9b95ce19db5abe0f376f75eb56517dff21e1a4751e2657d3e83d", size = 222838, upload-time = "2026-08-31T18:56:34.729Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/46/126b1831dca12060d4a8296bf9c4fe5c93c4f22197fa239cb0cc82042bba/filelock-3.32.6.tar.gz", hash = "sha256:a3f55a18af3652a94d8f47d6055df434f254ca1d02ef2524850c6d249ca2512c", size = 225172, upload-time = "2026-09-08T22:57:11.528Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/d2/b70a31e13d04456d28493f31d2aa087e99eeb2767ef0293b2625727ccb8c/filelock-3.32.5-py3-none-any.whl", hash = "sha256:142cd9fa77a872c5e78c62329a0d15278fadc686eb89e760017968961a4fd6b2", size = 100003, upload-time = "2026-08-31T18:56:33.078Z" }, + { url = "https://files.pythonhosted.org/packages/cc/06/4f138f618dbea66803291274f228f01daf29f306fe8b96bc30dab765df75/filelock-3.32.6-py3-none-any.whl", hash = "sha256:3f16ecd0117feae0dfc147e8c62eb5daeccd8bd800378c3ddf416de9b4feb6b1", size = 100189, upload-time = "2026-09-08T22:57:10.182Z" }, ] [[package]] @@ -1213,7 +1213,7 @@ wheels = [ [[package]] name = "google-cloud-storage" -version = "3.13.1" +version = "3.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core" }, @@ -1223,9 +1223,9 @@ dependencies = [ { name = "google-resumable-media" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/7e/73bb7512df1d1aad6ce3f9aed847cd40e0cd400ba4a85d86ab8eb412e9cc/google_cloud_storage-3.13.1.tar.gz", hash = "sha256:a80bf8cac2794808aa61c50c5f769ecbbe2d10331bacd0d69d30e59b14b346b2", size = 17341051, upload-time = "2026-08-06T06:24:42.229Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/06/33e124df40437c292c7d666616d890c12cfea5b83842acf5be7dc3fde016/google_cloud_storage-3.14.1.tar.gz", hash = "sha256:b24e74b493c60b19b83462933a83bb831e45b6ca4924b0d75eac8e176b58b3a7", size = 17348933, upload-time = "2026-09-08T17:10:11.871Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/6f/d69f0e185e08ddb58c323a0a935af2b492907b5de362bc08933b0a3b5644/google_cloud_storage-3.13.1-py3-none-any.whl", hash = "sha256:98208de6c21e85cecd3eb44551894efff33d98365500e178867d4305854a770a", size = 341486, upload-time = "2026-08-06T06:23:36.548Z" }, + { url = "https://files.pythonhosted.org/packages/ec/65/7201c1816ae46e0b4e70a3ce61e5121963315dc9df2e00fcb8897a3d0926/google_cloud_storage-3.14.1-py3-none-any.whl", hash = "sha256:8f0fe2b74bd80ddcbd67247a3b47b81035a0a4f448569ed358fa40a46c940118", size = 343228, upload-time = "2026-09-08T17:10:09.833Z" }, ] [[package]] @@ -2437,11 +2437,11 @@ wheels = [ [[package]] name = "narwhals" -version = "2.25.0" +version = "2.26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/7b/6248dada39781db1ab3ebf08943080df0796098515a87f6f8696d14ec744/narwhals-2.25.0.tar.gz", hash = "sha256:62c036c810662bf7820b7737077176313bc59350eeeefb808510f388c743e4b2", size = 677076, upload-time = "2026-08-20T18:10:15.454Z" } +sdist = { url = "https://files.pythonhosted.org/packages/35/68/5351e34623d253423240ea7de3f8fc74fa8ab14b1ab3c0ec4ac8997413c9/narwhals-2.26.0.tar.gz", hash = "sha256:6b9cadca82f375c7e4cf584fdc86ca25da54827307a9c58f94547ee6104b82dd", size = 686970, upload-time = "2026-09-08T13:32:08.964Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/dc/55481808fd70ef1567cf13540ffd4702af3f74b112e35427564b03f79c2d/narwhals-2.25.0-py3-none-any.whl", hash = "sha256:1f0f403e8c7e4463cde9bfe78b12fdd809e3ae3dda6d9b2f802934fb9c7a6a8f", size = 467373, upload-time = "2026-08-20T18:10:13.834Z" }, + { url = "https://files.pythonhosted.org/packages/40/b5/1b84b2c784db76d69442334bc8b8748c840f13ca53be086f4f250ad4a0bc/narwhals-2.26.0-py3-none-any.whl", hash = "sha256:29326d74f107c347fd1009bd58e38d9f7c7c5b51e6de97bc93dbc325d9038b54", size = 474034, upload-time = "2026-09-08T13:32:07.159Z" }, ] [[package]] @@ -2990,11 +2990,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.11.7" +version = "4.11.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/b7/802a56eca9f2fac455b8bab5375a2647b0f0e14a2cd63ef077de3c4a7658/platformdirs-4.11.7.tar.gz", hash = "sha256:4f41487eeeeeb07f3a6625e61d9bc0ae6809f92d3386dbd74392fbb76108104d", size = 35127, upload-time = "2026-09-01T13:35:10.502Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/18/f3bb8ef0d3b930692343da8aa4d3cbcd6749477c053959395ac81965a6e9/platformdirs-4.11.8.tar.gz", hash = "sha256:f23abafea7dd4276d1f29104b83598d7dcc567cafd07c9c951e66665645437fc", size = 37182, upload-time = "2026-09-08T22:20:42.866Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/6e/80993e10a0482f630cef528635789233224f36b1ffd11592aa15d13ff9ce/platformdirs-4.11.7-py3-none-any.whl", hash = "sha256:8a02cb259042c79d1cd0450facc2fe6dc9d303ae7901afbe33bf8ea0b188cef6", size = 23938, upload-time = "2026-09-01T13:35:09.02Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e1/5b7b8bbb55084d1425bcb9bc823ff519e1b2be05f6ebb0089e2eacc38413/platformdirs-4.11.8-py3-none-any.whl", hash = "sha256:52f2f181bbfde907966932cc8312d967d02976422d66d537ea16092b8e291081", size = 24027, upload-time = "2026-09-08T22:20:41.537Z" }, ] [[package]] @@ -4135,15 +4135,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.69.0" +version = "2.69.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3e/d4/911132f0ad673372159b4c47d3074444651b68c4c834fd5427f91285c66a/sentry_sdk-2.69.0.tar.gz", hash = "sha256:0cf7d29419145ab0250ea51363ad79fb7509996aaa35c5d5d2c3ed85b3b80a7d", size = 1042224, upload-time = "2026-09-08T08:16:29.258Z" } +sdist = { url = "https://files.pythonhosted.org/packages/10/16/85874f5e51f8d0767ee8c4b4c460c5ea2bc8a1b613d641d9d9577ba39d3a/sentry_sdk-2.69.1.tar.gz", hash = "sha256:f9284b417540b0784b994fa021eb6f1e30ae1cce593d83541274d03c93966eff", size = 1043599, upload-time = "2026-09-08T14:18:19.505Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/c4/11acfc646282156d8bec336db28143d33196531bd982fe20c2280f03abf6/sentry_sdk-2.69.0-py3-none-any.whl", hash = "sha256:3b92738027322061fbab34199102fcc0459ff9a5bf373ccca7091af9a5f07530", size = 528018, upload-time = "2026-09-08T08:16:27.203Z" }, + { url = "https://files.pythonhosted.org/packages/43/eb/17040f8a60c300fe4cc349088d3e351b88812f478b57a0af05a49af7cb7c/sentry_sdk-2.69.1-py3-none-any.whl", hash = "sha256:2d2556d9a14db548982b914cbed2a2dc07b6e55d941a620752f89a2afccfd78d", size = 528587, upload-time = "2026-09-08T14:18:18.151Z" }, ] [package.optional-dependencies] From 6657b0ff35c93eeb3c42ef9f75367491a35d44d5 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Wed, 9 Sep 2026 10:30:39 +0200 Subject: [PATCH 038/120] LCORE-3645: Class FeedbackCategory inherits from both str and enum --- src/models/common/feedback.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/common/feedback.py b/src/models/common/feedback.py index 432087315..eb353181c 100644 --- a/src/models/common/feedback.py +++ b/src/models/common/feedback.py @@ -1,9 +1,9 @@ """Predefined feedback categories for AI response quality signals.""" -from enum import Enum +from enum import StrEnum -class FeedbackCategory(str, Enum): +class FeedbackCategory(StrEnum): """Enum representing predefined feedback categories for AI responses. These categories help provide structured feedback about AI inference quality From 0340679e2a42a4c6bfe25eee648b2444d51dad17 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Wed, 9 Sep 2026 10:33:04 +0200 Subject: [PATCH 039/120] Removed redundant open mode arguments from sources --- src/client/ogx.py | 4 ++-- src/models/config.py | 2 +- src/ogx_configuration.py | 12 ++++++------ src/telemetry/configuration_snapshot.py | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/client/ogx.py b/src/client/ogx.py index 7d1edfd2a..3fcf6a51d 100644 --- a/src/client/ogx.py +++ b/src/client/ogx.py @@ -132,7 +132,7 @@ def _synthesize_library_config(self) -> str: "is not set" ) - with open(config_file, "r", encoding="utf-8") as f: + with open(config_file, encoding="utf-8") as f: lcs_config = yaml.safe_load(f) output_path = os.environ.get( @@ -158,7 +158,7 @@ def _load_service_client(self, config: OgxConfiguration) -> None: def _enrich_library_config(self, input_config_path: str) -> str: """Enrich OGX config with BYOK RAG and OKP Solr settings.""" try: - with open(input_config_path, "r", encoding="utf-8") as f: + with open(input_config_path, encoding="utf-8") as f: ls_config = yaml.safe_load(f) except (OSError, yaml.YAMLError) as e: logger.warning("Failed to read OGX config: %s", e) diff --git a/src/models/config.py b/src/models/config.py index d0a8298b7..7f60e09d4 100644 --- a/src/models/config.py +++ b/src/models/config.py @@ -1742,7 +1742,7 @@ def check_customization_model(self) -> Self: checks.file_check(self.agent_card_path, "agent card") try: - with open(self.agent_card_path, "r", encoding="utf-8") as f: + with open(self.agent_card_path, encoding="utf-8") as f: self.agent_card_config = yaml.safe_load(f) except yaml.YAMLError as e: raise ValueError( diff --git a/src/ogx_configuration.py b/src/ogx_configuration.py index 23d58201b..ea6309ecf 100644 --- a/src/ogx_configuration.py +++ b/src/ogx_configuration.py @@ -1010,7 +1010,7 @@ def load_default_baseline() -> dict[str, Any]: OSError: If the shipped baseline file cannot be read. yaml.YAMLError: If the baseline file is not valid YAML. """ - with open(DEFAULT_BASELINE_RESOURCE, "r", encoding="utf-8") as file: + with open(DEFAULT_BASELINE_RESOURCE, encoding="utf-8") as file: return yaml.safe_load(file) @@ -1257,7 +1257,7 @@ def synthesize_configuration( # pylint: disable=too-many-locals if unified and unified.get("profile"): profile_path = _resolve_profile_path(unified["profile"], config_file_dir) logger.info("Loading synthesis baseline from profile %s", profile_path) - with open(profile_path, "r", encoding="utf-8") as file: + with open(profile_path, encoding="utf-8") as file: baseline = yaml.safe_load(file) or {} elif unified and unified.get("baseline") == "empty": logger.info("Synthesizing from an empty baseline") @@ -1421,9 +1421,9 @@ def migrate_config_dumb( ValueError: If either input file does not parse to a mapping (e.g. an empty or comment-only file). """ - with open(run_yaml_path, "r", encoding="utf-8") as file: + with open(run_yaml_path, encoding="utf-8") as file: run_yaml = yaml.safe_load(file) - with open(lightspeed_yaml_path, "r", encoding="utf-8") as file: + with open(lightspeed_yaml_path, encoding="utf-8") as file: lcs_config = yaml.safe_load(file) # An empty or comment-only YAML file parses to None; fail with a clear @@ -1479,7 +1479,7 @@ def generate_configuration( """ logger.info("Reading OGX configuration from file %s", input_file) - with open(input_file, "r", encoding="utf-8") as file: + with open(input_file, encoding="utf-8") as file: ls_config = yaml.safe_load(file) dedupe_providers_vector_io(ls_config) @@ -1575,7 +1575,7 @@ def main() -> None: ) args = parser.parse_args() - with open(args.config, "r", encoding="utf-8") as f: + with open(args.config, encoding="utf-8") as f: config = yaml.safe_load(f) if has_synthesis_input(config): diff --git a/src/telemetry/configuration_snapshot.py b/src/telemetry/configuration_snapshot.py index 1bab8fa69..e7584e8c5 100644 --- a/src/telemetry/configuration_snapshot.py +++ b/src/telemetry/configuration_snapshot.py @@ -721,7 +721,7 @@ def _read_yaml_file(config_path: str) -> Any: The parsed YAML content, or None on failure. """ try: - with open(config_path, "r", encoding="utf-8") as f: + with open(config_path, encoding="utf-8") as f: return yaml.safe_load(f) except (OSError, yaml.YAMLError) as e: logger.warning("Failed to read OGX config for snapshot: %s", e) From 9d13daeb42d9f95b7030e36cb61ccf4b3412b622 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Wed, 9 Sep 2026 10:33:09 +0200 Subject: [PATCH 040/120] Removed redundant open mode arguments from tests --- tests/e2e/gen_scenario_list.py | 4 +-- tests/integration/test_unified_synthesis.py | 2 +- .../models/config/test_dump_configuration.py | 30 +++++++++---------- .../models/config/test_ogx_configuration.py | 2 +- tests/unit/models/config/test_vector_store.py | 2 +- .../unit/utils/dumpers/test_config_dumper.py | 2 +- .../unit/utils/dumpers/test_models_dumper.py | 4 +-- tests/unit/utils/test_checks.py | 2 +- tests/unit/utils/test_endpoints.py | 2 +- 9 files changed, 24 insertions(+), 26 deletions(-) diff --git a/tests/e2e/gen_scenario_list.py b/tests/e2e/gen_scenario_list.py index ac9587e06..19f70f284 100644 --- a/tests/e2e/gen_scenario_list.py +++ b/tests/e2e/gen_scenario_list.py @@ -52,9 +52,7 @@ if filename.endswith(".feature"): # feature file header print(f"## [`{filename}`]({FEATURES_URL_PREFIX}/{filename})\n") - with open( - os.path.join(FEATURE_DIRECTORY, filename), "r", encoding="utf-8" - ) as fin: + with open(os.path.join(FEATURE_DIRECTORY, filename), encoding="utf-8") as fin: for line in fin: line = line.strip() # process all scenarios and scenario outlines diff --git a/tests/integration/test_unified_synthesis.py b/tests/integration/test_unified_synthesis.py index 28a7d5d5a..fa7c8d1e6 100644 --- a/tests/integration/test_unified_synthesis.py +++ b/tests/integration/test_unified_synthesis.py @@ -168,7 +168,7 @@ def _legacy_enriched(tmp_path: Path, enrichment: dict[str, Any]) -> dict[str, An def _base_config_dict() -> dict[str, Any]: """Load the base lightspeed-stack.yaml fixture as a fresh dict.""" - with open(_BASE_CONFIG_PATH, "r", encoding="utf-8") as file: + with open(_BASE_CONFIG_PATH, encoding="utf-8") as file: return copy.deepcopy(yaml.safe_load(file)) diff --git a/tests/unit/models/config/test_dump_configuration.py b/tests/unit/models/config/test_dump_configuration.py index 15e3baee9..c02c42ca6 100644 --- a/tests/unit/models/config/test_dump_configuration.py +++ b/tests/unit/models/config/test_dump_configuration.py @@ -110,7 +110,7 @@ def test_dump_configuration_minimal_cfg(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None @@ -336,7 +336,7 @@ def test_dump_configuration_valid_values(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None @@ -552,7 +552,7 @@ def test_dump_configuration_with_one_mcp_server(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) assert content is not None assert "mcp_servers" in content @@ -604,7 +604,7 @@ def test_dump_configuration_with_more_mcp_servers(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) assert content is not None assert "mcp_servers" in content @@ -715,7 +715,7 @@ def test_dump_configuration_with_quota_limiters(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None @@ -995,7 +995,7 @@ def test_dump_configuration_with_quota_limiters_different_values( dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None @@ -1230,7 +1230,7 @@ def test_dump_configuration_with_vector_store(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) assert content["vector_store"] == { @@ -1310,7 +1310,7 @@ def test_dump_configuration_byok(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None @@ -1566,7 +1566,7 @@ def test_dump_configuration_pg_namespace(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None @@ -1807,7 +1807,7 @@ def test_dump_configuration_with_one_skill(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None @@ -1884,7 +1884,7 @@ def test_dump_configuration_with_skills(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None @@ -1962,7 +1962,7 @@ def test_dump_configuration_allow_degraded_mode(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None @@ -2206,7 +2206,7 @@ def test_dump_configuration_max_retries_settings(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None @@ -2450,7 +2450,7 @@ def test_dump_configuration_retry_count_settings(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None @@ -2700,7 +2700,7 @@ def test_dump_configuration_specific_compaction_values(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None diff --git a/tests/unit/models/config/test_ogx_configuration.py b/tests/unit/models/config/test_ogx_configuration.py index 93183f77b..37ea25336 100644 --- a/tests/unit/models/config/test_ogx_configuration.py +++ b/tests/unit/models/config/test_ogx_configuration.py @@ -24,7 +24,7 @@ def _base_config_dict() -> dict[str, Any]: """Load the base lightspeed-stack.yaml fixture as a fresh dict.""" - with open(_BASE_CONFIG_PATH, "r", encoding="utf-8") as file: + with open(_BASE_CONFIG_PATH, encoding="utf-8") as file: return copy.deepcopy(yaml.safe_load(file)) diff --git a/tests/unit/models/config/test_vector_store.py b/tests/unit/models/config/test_vector_store.py index 4689da0ad..830472d07 100644 --- a/tests/unit/models/config/test_vector_store.py +++ b/tests/unit/models/config/test_vector_store.py @@ -17,7 +17,7 @@ def _base_config_dict() -> dict[str, Any]: """Load the base lightspeed-stack.yaml fixture as a fresh dict.""" - with open(_BASE_CONFIG_PATH, "r", encoding="utf-8") as file: + with open(_BASE_CONFIG_PATH, encoding="utf-8") as file: return copy.deepcopy(yaml.safe_load(file)) diff --git a/tests/unit/utils/dumpers/test_config_dumper.py b/tests/unit/utils/dumpers/test_config_dumper.py index c30c2bbd7..1b07914be 100644 --- a/tests/unit/utils/dumpers/test_config_dumper.py +++ b/tests/unit/utils/dumpers/test_config_dumper.py @@ -47,7 +47,7 @@ def test_dump_schema(tmpdir: Path) -> None: filename = tmpdir / "foo.json" dump_schema(str(filename)) - with open(filename, "r", encoding="utf-8") as fin: + with open(filename, encoding="utf-8") as fin: # schema should be stored in JSON format content = load(fin) assert content is not None diff --git a/tests/unit/utils/dumpers/test_models_dumper.py b/tests/unit/utils/dumpers/test_models_dumper.py index 6d4834cd6..684a61c89 100644 --- a/tests/unit/utils/dumpers/test_models_dumper.py +++ b/tests/unit/utils/dumpers/test_models_dumper.py @@ -10030,7 +10030,7 @@ def test_dump_models(tmpdir: Path) -> None: filename = tmpdir / "foo.json" dump_models(str(filename)) - with open(filename, "r", encoding="utf-8") as fin: + with open(filename, encoding="utf-8") as fin: # schema should be stored in JSON format content = load(fin) assert content is not None @@ -10279,7 +10279,7 @@ def test_dump_models(tmpdir: Path) -> None: def check_json_file_content(filename: Path, expected_schemas: list[str]) -> None: """Check the content of provided JSON file with OpenAPI-compatible schema.""" - with open(filename, "r", encoding="utf-8") as fin: + with open(filename, encoding="utf-8") as fin: # schema should be stored in JSON format content = load(fin) assert content is not None diff --git a/tests/unit/utils/test_checks.py b/tests/unit/utils/test_checks.py index 0990c66e1..a9dff5e5f 100644 --- a/tests/unit/utils/test_checks.py +++ b/tests/unit/utils/test_checks.py @@ -15,7 +15,7 @@ def input_file_fixture(tmp_path: Path) -> str: """Create file manually using the tmp_path fixture.""" filename = os.path.join(tmp_path, "mydoc.csv") - with open(filename, "wt", encoding="utf-8") as fout: + with open(filename, "w", encoding="utf-8") as fout: fout.write("some content!") return filename diff --git a/tests/unit/utils/test_endpoints.py b/tests/unit/utils/test_endpoints.py index f7e10e39b..9e88cf489 100644 --- a/tests/unit/utils/test_endpoints.py +++ b/tests/unit/utils/test_endpoints.py @@ -23,7 +23,7 @@ def input_file_fixture(tmp_path: Path) -> str: """Create file manually using the tmp_path fixture.""" filename = os.path.join(tmp_path, "prompt.txt") - with open(filename, "wt", encoding="utf-8") as fout: + with open(filename, "w", encoding="utf-8") as fout: fout.write("this is prompt!") return filename From 47700e07240d8dc81e21464cc8c67e9f177c5472 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Wed, 9 Sep 2026 13:49:40 +0200 Subject: [PATCH 041/120] LCORE-4062: Fixed CVE found in NLTK package --- .konflux/requirements.hashes.wheel.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.konflux/requirements.hashes.wheel.txt b/.konflux/requirements.hashes.wheel.txt index 8fc832288..dcd9a4d48 100644 --- a/.konflux/requirements.hashes.wheel.txt +++ b/.konflux/requirements.hashes.wheel.txt @@ -215,8 +215,8 @@ narwhals==2.24.0 \ --hash=sha256:30285014e6754ed468b29358bc0aebff0117d6a1d64383254ae55ceef31ffb25 networkx==3.6.1 \ --hash=sha256:56b687ad58bed743066f1b7d5e6f56a72d2f193c8eaf35064abaedda4fba3745 -nltk==3.10.0 \ - --hash=sha256:a15da2911adca5c7b4574b902f6b344f3f65db72ded7f79b30617bbe7100d318 +nltk==3.10.3 \ + --hash=sha256:fb9ae77ba878bf1f09e6cca649f2c296f20b1c83143932ae06c12018b505afd7 numpy==2.3.5 \ --hash=sha256:927205c8882f7a53543a8fc5b13fcb05771d6bfff35e06daf8030e57549cc7be \ --hash=sha256:a08c6c26c26d7530d09ac93bc177593958c2e3ce4a9b5750c14a80329cca157c From 7d29bf19760687cb37621863faff5348a4056837 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Thu, 10 Sep 2026 08:38:28 +0200 Subject: [PATCH 042/120] LCORE-3646: Class HealthStatus inherits from both str and enum --- src/models/common/health.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/common/health.py b/src/models/common/health.py index 33c574a51..ec52db2de 100644 --- a/src/models/common/health.py +++ b/src/models/common/health.py @@ -1,12 +1,12 @@ """Health-related shared models for readiness and diagnostics.""" -from enum import Enum +from enum import StrEnum from typing import Optional from pydantic import BaseModel, Field -class HealthStatus(str, Enum): +class HealthStatus(StrEnum): """Health status enum for provider and service health checks. This enum serves two purposes: From 46dec7aa7efb23c4983bb6b470ac40a9e1156b2c Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Thu, 10 Sep 2026 08:45:45 +0200 Subject: [PATCH 043/120] LCORE-3649: Class AgentFinishReason inherits from both str and enum --- src/utils/agents/query.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/agents/query.py b/src/utils/agents/query.py index d39634be2..fb78c1dac 100644 --- a/src/utils/agents/query.py +++ b/src/utils/agents/query.py @@ -2,7 +2,7 @@ from __future__ import annotations -from enum import Enum +from enum import StrEnum from typing import Optional from fastapi import HTTPException @@ -60,7 +60,7 @@ tracer = trace.get_tracer(__name__) -class AgentFinishReason(str, Enum): +class AgentFinishReason(StrEnum): """Finish reason for a completed agent model response.""" CONTENT_FILTER = "content_filter" From 735b996aaa1c284d3e77ceb8423222bd69b617dd Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Thu, 10 Sep 2026 08:41:02 +0200 Subject: [PATCH 044/120] LCORE-3647: Class JsonPathOperator inherits from both str and enum --- src/models/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/config.py b/src/models/config.py index 7f60e09d4..76669368d 100644 --- a/src/models/config.py +++ b/src/models/config.py @@ -4,7 +4,7 @@ import os import re -from enum import Enum +from enum import Enum, StrEnum from functools import cached_property from pathlib import Path from re import Pattern @@ -1140,7 +1140,7 @@ def check_storage_location_is_set_when_needed(self) -> Self: return self -class JsonPathOperator(str, Enum): +class JsonPathOperator(StrEnum): """Supported operators for JSONPath evaluation. Note: this is not a real model, just an enumeration of all supported JSONPath operators. From b2a56e9be010c0fc849f924b6d2d04b5f2f2e7d9 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Thu, 10 Sep 2026 08:43:54 +0200 Subject: [PATCH 045/120] LCORE-3648: Class Action inherits from both str and enum --- src/models/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/config.py b/src/models/config.py index 7f60e09d4..1608b5841 100644 --- a/src/models/config.py +++ b/src/models/config.py @@ -4,7 +4,7 @@ import os import re -from enum import Enum +from enum import Enum, StrEnum from functools import cached_property from pathlib import Path from re import Pattern @@ -1274,7 +1274,7 @@ def compiled_regex(self) -> Optional[Pattern[str]]: return None -class Action(str, Enum): +class Action(StrEnum): """Available actions in the system. Note: this is not a real model, just an enumeration of all action names. From c72a3f2f8b80779b489cd69adf2652118478c7c8 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Thu, 10 Sep 2026 12:48:24 +0200 Subject: [PATCH 046/120] LCORE-3819: Removed unused import --- src/models/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/config.py b/src/models/config.py index 0d0622b6d..66825ed86 100644 --- a/src/models/config.py +++ b/src/models/config.py @@ -4,7 +4,7 @@ import os import re -from enum import Enum, StrEnum +from enum import StrEnum from functools import cached_property from pathlib import Path from re import Pattern From a7ff6468ad8376e4e61f9f5e6ebdf8d68434d532 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Thu, 10 Sep 2026 14:59:57 +0200 Subject: [PATCH 047/120] LCORE-4086: Fixed Cryptography CVE --- .konflux/requirements.hashes.wheel.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.konflux/requirements.hashes.wheel.txt b/.konflux/requirements.hashes.wheel.txt index dcd9a4d48..51c516820 100644 --- a/.konflux/requirements.hashes.wheel.txt +++ b/.konflux/requirements.hashes.wheel.txt @@ -59,9 +59,9 @@ chevron==0.14.0 \ --hash=sha256:4cbb3abb3803127a24947e075384f21bde2e00d43b8b8026f4ba9af7f866a423 click==8.4.2 \ --hash=sha256:368d4af9976de9fc5efc4d8a26c64d23c187c21a2eba9336254832d466dd6198 -cryptography==49.0.0 \ - --hash=sha256:272f6cf35bd11146fb7182ed94b7123b9547174e944a0ec41fb547d1ad363982 \ - --hash=sha256:44e7dda87cce4e64a4eada2469ade616471772ee8203d7331967ef7ba2fee2bd +cryptography==50.0.1 \ + --hash=sha256:3e29639a33400721f710beeba7741e275bdb4c5bc53107c9dfef240d251c0590 \ + --hash=sha256:8ae962f84e7f36290feca5f8ba7bfc01fea9cd9a48f446bca52ef115657cca3d datasets==5.0.0 \ --hash=sha256:0fbc08ef020b03a22d91ad38b3b22091a1e18c1335f2963d357ea540bfb032e7 defusedxml==0.7.1 \ From 9ea69a9ba41589f95466906c37c710611c5064bf Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Fri, 11 Sep 2026 08:46:38 +0200 Subject: [PATCH 048/120] LCORE-3650 --- src/utils/stream_interrupts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/stream_interrupts.py b/src/utils/stream_interrupts.py index 3d515e2e8..df075025b 100644 --- a/src/utils/stream_interrupts.py +++ b/src/utils/stream_interrupts.py @@ -4,7 +4,7 @@ import datetime from collections.abc import Callable, Coroutine from dataclasses import dataclass, field -from enum import Enum +from enum import StrEnum from threading import Lock from typing import Any, Optional, cast @@ -53,7 +53,7 @@ class ActiveStream: conversation_id: Optional[str] = None -class CancelStreamResult(str, Enum): +class CancelStreamResult(StrEnum): """Outcomes when attempting to cancel a stream.""" CANCELLED = "cancelled" From 0c691392368ba9e10f64eb06d70534de3308ec20 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Fri, 11 Sep 2026 08:52:48 +0200 Subject: [PATCH 049/120] LCORE-3580: Updated dependencies --- uv.lock | 519 +++++++++++++++++++++++++++----------------------------- 1 file changed, 254 insertions(+), 265 deletions(-) diff --git a/uv.lock b/uv.lock index ea78812e1..f06d161f5 100644 --- a/uv.lock +++ b/uv.lock @@ -30,7 +30,7 @@ wheels = [ [[package]] name = "accelerate" -version = "1.14.0" +version = "1.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, @@ -42,9 +42,9 @@ dependencies = [ { name = "torch", version = "2.11.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, { name = "torch", version = "2.11.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8d/75/94cd5d389649578aca399e5aa822637eec18319a1dadc400ffe2f9a7493f/accelerate-1.14.0.tar.gz", hash = "sha256:41b9c4377a54e0b460a959b0defa1b736e4ca0a2373252d9a539964c2afe3c8d", size = 412167, upload-time = "2026-06-11T13:45:52.326Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/b5/1d3ed029ac71d3f2961346829a268da923698e9fd63f218f78841f216bfd/accelerate-1.15.0.tar.gz", hash = "sha256:5654f8c5eaa0d4fa68b33e287a97765da6849bf6d51dcac874e73fbbddfb6134", size = 422615, upload-time = "2026-09-09T13:04:49.078Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/db/253133d7e7cb40d3af384bb2f5c0b4a2b7fdcffbc95c688cc67a20a3c103/accelerate-1.14.0-py3-none-any.whl", hash = "sha256:e94390c2863b873be18f623f9df48a0d8fe5eff13ea7f1a00092b0a7904888c6", size = 389246, upload-time = "2026-06-11T13:45:50.477Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4c/34f0450479d01195027260da68d8a3880683f1640c3ca5adf64acb3185f1/accelerate-1.15.0-py3-none-any.whl", hash = "sha256:97eacca0b73e45cb867dbf8c5d5d4dc32219544300e0c8992c7334dc2ef33cec", size = 394295, upload-time = "2026-09-09T13:04:47.331Z" }, ] [[package]] @@ -169,7 +169,7 @@ wheels = [ [[package]] name = "anthropic" -version = "1.4.0" +version = "1.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -180,9 +180,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/75/6d/793f5cfe2cd444c43b4eeb4cb7c3cc55ebcb38929fdfe81aa1f2fced7326/anthropic-1.4.0.tar.gz", hash = "sha256:f0d017e901e48b343520b5d458f8240c283c8d850bf6d119834c622207e0a74c", size = 1150831, upload-time = "2026-09-04T22:20:31.355Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/43/6f3f6006f5216d43a059a1a856d275ef6536cdfa94883b64cc04d7873cb7/anthropic-1.5.0.tar.gz", hash = "sha256:b25f87f5758861f25993383a5c9bf274eb6e0f1b010c84019ad91854cb4e1bc6", size = 1156657, upload-time = "2026-09-10T17:45:35.93Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/e3/34a88f0e1e854022a352d67f72ca9baa8b952d4541315088411ba2bfbc2a/anthropic-1.4.0-py3-none-any.whl", hash = "sha256:590e85bff75b713a123b03f586d68f02266b5fdc49f70dd75f721ced93a4716c", size = 1300390, upload-time = "2026-09-04T22:20:33.078Z" }, + { url = "https://files.pythonhosted.org/packages/c2/1c/c32fce35ca0be0205f2377d2238e3ed73dea5abc2be09d15fbd032091d60/anthropic-1.5.0-py3-none-any.whl", hash = "sha256:d9ce04b29ad1f7025dda3e3bc478cde8d2924d2de5417d2d4a4bb0378ecbc2a6", size = 1235236, upload-time = "2026-09-10T17:45:34.216Z" }, ] [[package]] @@ -218,46 +218,46 @@ wheels = [ [[package]] name = "ast-serialize" -version = "0.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7a/c2/feb42ca5bf2335aa16868bc53ec83f114923564763658d6a63e4c4c6ccde/ast_serialize-0.11.0.tar.gz", hash = "sha256:8b4b9862436eaf1442d6a16b7e138d4ff6ae558bdb6fde1c4cd87735093d5744", size = 953724, upload-time = "2026-09-08T14:58:39.804Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e1/cf820beb541ab4177a3566be92478e2f5cadfc1306c0cc5300c174dde1a3/ast_serialize-0.11.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:75d12e18c8d6691ceda09f514de2effcac628b217c0b3ea17851d217bfdbdab8", size = 1234341, upload-time = "2026-09-08T14:57:47.197Z" }, - { url = "https://files.pythonhosted.org/packages/89/01/8859ba83facbb1d57720eec1262d01547df3f9269d527eba36b785137a29/ast_serialize-0.11.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:ee8928ae9305f921484239a97030146dbb674cc10327fdf84124c4a20f73382e", size = 1212409, upload-time = "2026-09-08T14:57:48.459Z" }, - { url = "https://files.pythonhosted.org/packages/91/b5/2938098537ea94c204f85cc34acc41fec8a7bd3986b8d617f84cf25e802c/ast_serialize-0.11.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c345fb4e035ca5db948b781d894f5d98d8a271497ce0fee319f9375d5d358c9b", size = 1280678, upload-time = "2026-09-08T14:57:49.787Z" }, - { url = "https://files.pythonhosted.org/packages/41/7b/4d36b08ecad936af6f09dac75ed30198ec9f6c615ffe9e478ed6dd1450bc/ast_serialize-0.11.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7523fce1258c810c277799557771e35a851e5c1f132d0a68ca4551675f868b7", size = 1286678, upload-time = "2026-09-08T14:57:51.106Z" }, - { url = "https://files.pythonhosted.org/packages/95/ea/2fc504ddb5ed27c78f9de75c1835c365973d16d8d7f94a10ddf47f903c42/ast_serialize-0.11.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d861d8232f6c1661b805f22c96c3923a76c8feed1a4e504cecc0eaa3cd4077e", size = 1556836, upload-time = "2026-09-08T14:57:52.564Z" }, - { url = "https://files.pythonhosted.org/packages/c0/56/36bd4b5a73178ffede18e20020a73e153809b4d4d44ba8b5cfa2f80be6d6/ast_serialize-0.11.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:93206e8059d23952475099967174b7a3f573ca7dc19bdcaa59dbfbf86d76e52b", size = 1301292, upload-time = "2026-09-08T14:57:54.052Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b8/88fb5bb26cf5614174b74172118461c17511dbbd8f827f0bde4da7280da2/ast_serialize-0.11.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18e28f523669d07547795aac3d9a5cf9e7c8d6ddf3dd48fe2090009ab35d0f69", size = 1299691, upload-time = "2026-09-08T14:57:55.67Z" }, - { url = "https://files.pythonhosted.org/packages/81/45/382d0e3426efefda064ef24036c2ca5905f4e8efeadd87ef73df55e010da/ast_serialize-0.11.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:cd6b802249a22d0f44cee22452910a653493e4b974a8dbe401f6b85cb4d74219", size = 1308581, upload-time = "2026-09-08T14:57:57.061Z" }, - { url = "https://files.pythonhosted.org/packages/43/d0/71e43cd333fe46c984233819891a22648d4c7c1c3a7ae98d6e3945386c81/ast_serialize-0.11.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3f552edbf9e7e0fe69b1eda569cf775c2279e5f0b839727d63c39a73adaef966", size = 1356809, upload-time = "2026-09-08T14:57:58.494Z" }, - { url = "https://files.pythonhosted.org/packages/22/bc/b916b5a3ce58a2b540611dd7ece93e4015c30328f3d6459f79fc252d0825/ast_serialize-0.11.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:0ecc5e1282b8ce7c73fc204bde4eb3bad500784e1f0e14f0b9ea624124095821", size = 1458221, upload-time = "2026-09-08T14:57:59.798Z" }, - { url = "https://files.pythonhosted.org/packages/b4/bc/3173921ff69ad214bc54172c572719f792faf118bca3485f412bbf26716f/ast_serialize-0.11.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:2110f04937ac932c03839af266a57a8b29fd97b0789d500fa92782bdad70f475", size = 1562074, upload-time = "2026-09-08T14:58:01.096Z" }, - { url = "https://files.pythonhosted.org/packages/4f/69/49cbb574085b5d08f9384d1066f5041fea42e0d79d1f4f39c95cafb54897/ast_serialize-0.11.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:2fc6fcdbf95a358f3be9af5ee8ccad40e0ef61ed6b84b942003e5bd983ac4cc9", size = 1556027, upload-time = "2026-09-08T14:58:02.581Z" }, - { url = "https://files.pythonhosted.org/packages/cb/59/13a559d9a28ef6ea38904ee4e766b49860ba046a12842d7620668f3c57b7/ast_serialize-0.11.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:6a5879e45a85c0cce453c1302250a9d644bab59c2d615e6720da03c2d1baffe3", size = 1689378, upload-time = "2026-09-08T14:58:03.889Z" }, - { url = "https://files.pythonhosted.org/packages/da/20/77d92e56456dc047c673fd9d1d2adfb64d767479ad4a4e1d7089e95030c0/ast_serialize-0.11.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:6cf3edf38d808e3f26094473f593feb3c05d68d4c53b366c9c3a1c3284438f71", size = 1482258, upload-time = "2026-09-08T14:58:05.219Z" }, - { url = "https://files.pythonhosted.org/packages/8c/41/eca650f9ed98f3c84f8d498b8f1268388090b71606afb0dede4eb977b449/ast_serialize-0.11.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:c912d84a7eed20aa51ce864a37e60c275a3c4f972056aab20247ec8d01b8457e", size = 1499691, upload-time = "2026-09-08T14:58:06.772Z" }, - { url = "https://files.pythonhosted.org/packages/9d/79/94e77924e793ca881d240f71fd5cf0929f05bcaf8e293b134ef8254643ad/ast_serialize-0.11.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:f504a2c787d22822fcfa4297bbb014cef1dc06962a30d2710c8c1cd5cf8326ec", size = 1118536, upload-time = "2026-09-08T14:58:08.478Z" }, - { url = "https://files.pythonhosted.org/packages/be/f1/024c32d13b5c62df0bbc4bb37eea7b1b49068d77bacfbb1e82a83fb2260e/ast_serialize-0.11.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:d0ee62b0149144785e2ea158cade30a41d90446cbaeae5745cb996adcee99c68", size = 1155492, upload-time = "2026-09-08T14:58:10.016Z" }, - { url = "https://files.pythonhosted.org/packages/cd/14/671534a8129604577e1c1ada3f21b32982c46d18c21e4e3511c2da8011bf/ast_serialize-0.11.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:2468e68d7f5b6d80fb619f614734f46ab8c2d474437bcba16cf3f63b5e1b3f79", size = 1126547, upload-time = "2026-09-08T14:58:11.361Z" }, - { url = "https://files.pythonhosted.org/packages/a5/52/0e5e3afe70f9bded7a4643734c685ddd2e33932e3fed9b29836fc87238fc/ast_serialize-0.11.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d2e3fb7476f2ccc2c2e40155f8e04e6110be98b6f51e52cc5476f3be5b1a1956", size = 1238152, upload-time = "2026-09-08T14:58:14.081Z" }, - { url = "https://files.pythonhosted.org/packages/6a/91/0fc54081b97feb2fe440aa1b84f32920608fb5b93a25b42cee0b55a4900a/ast_serialize-0.11.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:fc0a2fc3335a72a750c84e2a8cf113e4265edc930ac1b64b5f52fb7685070e3a", size = 1227156, upload-time = "2026-09-08T14:58:15.406Z" }, - { url = "https://files.pythonhosted.org/packages/96/8b/fe9a2da2839f2b94db49ebbd3404aefc43daaa91d250116c1a8817b908e9/ast_serialize-0.11.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26803ffd2daac676b4acf6683d527af091ed2fbf1c7eca861216702c3296d408", size = 1289363, upload-time = "2026-09-08T14:58:16.804Z" }, - { url = "https://files.pythonhosted.org/packages/cb/41/b4d53de75b60e5b4bdffab76acfd4d25cde840080a1f5741f37f8d6dad42/ast_serialize-0.11.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c394f64c4a7aba67173dda8b70087b37c82dc899265dca160a9ccd0d2c9e1ff5", size = 1293293, upload-time = "2026-09-08T14:58:18.174Z" }, - { url = "https://files.pythonhosted.org/packages/b1/d6/337593438d847e78c93f26fc2eb670680759fe9c19e0975a82720e850b0c/ast_serialize-0.11.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce307fe34458a9fb87c660730796e55f977acf5c4e9c645a806faf3436d402a9", size = 1564305, upload-time = "2026-09-08T14:58:19.481Z" }, - { url = "https://files.pythonhosted.org/packages/cd/51/40aec6d8c3afc2e6cba24fe496ae43b43dfdcb3f84de96e0746bc4f83cd6/ast_serialize-0.11.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82f67de691ae29aa44afc6fff5d7c77f93bd5218954757a9453ebdbe924cfb12", size = 1307996, upload-time = "2026-09-08T14:58:20.91Z" }, - { url = "https://files.pythonhosted.org/packages/6a/7a/53e67994ed4d19972f958cc69759caf71c28d3f7d094152affc1d47006c7/ast_serialize-0.11.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2605d538e17e9569643b5c0722da8d39b1d758391e2a1902f44abfe42b8f7f78", size = 1309997, upload-time = "2026-09-08T14:58:22.463Z" }, - { url = "https://files.pythonhosted.org/packages/b9/50/d3555436b54feb7a11affe579113f6fcacd848be2a505592162e5930fcca/ast_serialize-0.11.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:8dcae98b677ca0ed9988d09b83394db65385ca6f45b0ee8892e68c90e5d8df74", size = 1318566, upload-time = "2026-09-08T14:58:24.106Z" }, - { url = "https://files.pythonhosted.org/packages/a1/f6/e45e90b6c7798d9c042c3e8c12357edd16131f7ea779228d79caf0bb858c/ast_serialize-0.11.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a2ed2669adf5e92f4d0cfd70a72c8f949c66309657eabb54a2c7a0ba2577f8d", size = 1364618, upload-time = "2026-09-08T14:58:25.582Z" }, - { url = "https://files.pythonhosted.org/packages/71/90/2f8705717d6816ed77d2378f118b9e7664dc73a2d34bd2a668dc0b8e7bfd/ast_serialize-0.11.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:60d3fd70362b539318e1149ca09df0d0f9ff03e7ea72b117453003a9d8598ab7", size = 1466509, upload-time = "2026-09-08T14:58:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/80/72/fde49aae39cfddf9736df4911e12f66120c99ff6e002690e50cd24697417/ast_serialize-0.11.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1241b3963a07bd474f0a5d53f054bbbda278c805efd1d7b4ce41d0d3c5becc1e", size = 1571073, upload-time = "2026-09-08T14:58:28.444Z" }, - { url = "https://files.pythonhosted.org/packages/25/87/f5a9d0ce56d417ea831d4627358884af3695db682f7f7bb167751e79f891/ast_serialize-0.11.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:a2fbc5cbff379354d6e07296f0f953955543f60134ffd91fb8f14fc0b4f1aeaa", size = 1566781, upload-time = "2026-09-08T14:58:29.713Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b0/af07c6974ef839dd2d4f2a2123e106a9a484a42786855b7a59281f8fe1f3/ast_serialize-0.11.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc0c793063fe07e52244c66ceb2b2c5eecaa826dbb250fdeb7f0502e5697782", size = 1696239, upload-time = "2026-09-08T14:58:30.948Z" }, - { url = "https://files.pythonhosted.org/packages/20/67/c8b17c359650809ee00badf2179d90222f09e889662bead41a4695b0c00c/ast_serialize-0.11.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:d475173e59fec5dd1bad84d8351c7f5230119b1d5c68bad93264657589a93043", size = 1491560, upload-time = "2026-09-08T14:58:32.576Z" }, - { url = "https://files.pythonhosted.org/packages/04/71/29ca37b4bc78d924e54d2a5d4daa6d5a142daf0b7f614996b651225079e3/ast_serialize-0.11.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:26bd819c06ee10df68dbcf2fb12c8fd2249bcadffe25ba7011f0686cbfd69b94", size = 1507743, upload-time = "2026-09-08T14:58:33.827Z" }, - { url = "https://files.pythonhosted.org/packages/54/5b/8f3381e22b86e3bdb74898ea4f7a15b54cca308a63e9e02753f99d6dd4fe/ast_serialize-0.11.0-cp39-abi3-win32.whl", hash = "sha256:4c4c2df749d1dc6bba9a9e8cc4b5c3dfc39a351fbfe4ce22ca49c2d488c7a7ce", size = 1124421, upload-time = "2026-09-08T14:58:35.332Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b0/1ddfbc7aec32df2ed87ad830ee55854ddeceb2e8a8fcfae9b6ed57be0b4b/ast_serialize-0.11.0-cp39-abi3-win_amd64.whl", hash = "sha256:dd3c69e27b1be3172880bce2867c359106c28c5909f1ca6154bb7bf89847c419", size = 1162052, upload-time = "2026-09-08T14:58:36.674Z" }, - { url = "https://files.pythonhosted.org/packages/34/0b/32f3c8162cb5b33f24bea94503dbdd6b55aee72d367d23a55f1338f3e1b7/ast_serialize-0.11.0-cp39-abi3-win_arm64.whl", hash = "sha256:eb22d9300e7a064fa8c45e2c1e568a4e36c4c91487ea0da5e7f589905365c866", size = 1134422, upload-time = "2026-09-08T14:58:38.267Z" }, +version = "0.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/a2/04a9383e7512c91c54b2f34b3ff86dc7d2610506f588c2bda36e952a68f7/ast_serialize-0.11.1.tar.gz", hash = "sha256:cc5db2983805f6be786488aac8c5998d5b71965488d1b18c44d435a2205a5cb4", size = 953785, upload-time = "2026-09-09T16:05:27.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/dd/16de2c0d23a6b298c735d4e4b56d86fa70476eef98e2c66ceaa65503b62f/ast_serialize-0.11.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:a015ede631eeb098a23de8ec0cfe11a1dd02159ab7a26bdc7c6bde2befbbc7b4", size = 1235495, upload-time = "2026-09-09T16:04:29.654Z" }, + { url = "https://files.pythonhosted.org/packages/27/f7/302d2251e6298bbeabc8a1815c9147127031517b305001a02f34fd46b6b5/ast_serialize-0.11.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:57b7d52d1f5c92905cd648bda9efc83cac33097de1e5bb68de8feca9b5f7e87a", size = 1215642, upload-time = "2026-09-09T16:04:31.186Z" }, + { url = "https://files.pythonhosted.org/packages/93/06/93b6527646613502f364cebfb23783fa6701cdbe90761ee26144f1fa20b9/ast_serialize-0.11.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e47b9b028efbc0486263a49f782ad0ae3e879855fe5a2897b59feefb78e1c40c", size = 1283526, upload-time = "2026-09-09T16:04:32.597Z" }, + { url = "https://files.pythonhosted.org/packages/2f/ac/ffb216262c9a582d039d846081b8c3017dc368cbed5a2520deca5cb7f8cb/ast_serialize-0.11.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00adb748034c7b1938ec56925f9f6230ffcd9c56d47355fe32485df29b90b977", size = 1287526, upload-time = "2026-09-09T16:04:34.214Z" }, + { url = "https://files.pythonhosted.org/packages/a2/06/19a4c4837c4d5e73b982938d6da3c9ae10513b61527575b3e1b8396f9298/ast_serialize-0.11.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:82359d00ea808c260c327e43955c0190bb7baef152a01ae0b5c74d05387b9552", size = 1558354, upload-time = "2026-09-09T16:04:35.669Z" }, + { url = "https://files.pythonhosted.org/packages/e5/29/2373199907b2d97feed176115308dfb3dd0446566b047009828f266d2d66/ast_serialize-0.11.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f35bb9ddeed4008c7d3e272dd1999885d712e1ce623b0a65ba8a8ed1d51c35cf", size = 1302731, upload-time = "2026-09-09T16:04:37.423Z" }, + { url = "https://files.pythonhosted.org/packages/33/55/ab6805a1457dbe565c84f8d36e2aa4207ef22408adfd01d225823cd07484/ast_serialize-0.11.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74093ed682ba58456f0d4450da7fb62b826fddd97824d44fc4a81ca6e0bf9e23", size = 1301594, upload-time = "2026-09-09T16:04:38.861Z" }, + { url = "https://files.pythonhosted.org/packages/52/e2/4f54eab2201bb420d39bf61423abcb5d4daffd28e2270e1c5eee2bf3c0f1/ast_serialize-0.11.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:4dd7218870c203eff4533cfc04a39f29077d6789b20f5b746e7648fe5762a548", size = 1309355, upload-time = "2026-09-09T16:04:40.543Z" }, + { url = "https://files.pythonhosted.org/packages/10/07/755dd98664e2374080b72ccb5fea63d9f11b4bec2409a05b2a4ebc97618d/ast_serialize-0.11.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e8d20700766171a8a17f89cf53d7bd9712c509460d28a51f7f98340577b78aa", size = 1356645, upload-time = "2026-09-09T16:04:42.299Z" }, + { url = "https://files.pythonhosted.org/packages/34/a0/fadbde3e108064236e2679728ce6d8247aefc81bac5fd82505b59cf69172/ast_serialize-0.11.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:20b3371c0403099cd55d59f4bcedb6d3994d9db3fa41711c648760a89ef2a575", size = 1459696, upload-time = "2026-09-09T16:04:44.069Z" }, + { url = "https://files.pythonhosted.org/packages/02/fd/b4f95249bd895368ea8290668e27d1f0a5c4ad888bd72a34c7beb2a500d6/ast_serialize-0.11.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:cc1a78b913f8665dda145999b1b4805ad2ef442ecbe3b819512cf7f95e70498a", size = 1562517, upload-time = "2026-09-09T16:04:45.461Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/6afac594380410710d9d863b9ef41e9c6ca89bd901bda1464ee9e99180a8/ast_serialize-0.11.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:38f7141881e783bccc362d201d7fd7437934216b669b9fef39febc2f63fd2d9b", size = 1556951, upload-time = "2026-09-09T16:04:47.12Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/5a81b5cbf1731f4722bc90adf4b4ace7dda69ca29837844147063aeafb22/ast_serialize-0.11.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:13903a3f212ab9e5d05a6c3f2337288f4f8f4c18e6ab3817417ae7a3d46dee14", size = 1691039, upload-time = "2026-09-09T16:04:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/99/eb/d9b903206cbd6d5143838367d385fa88ec49eaae2cc12f284327c6e0af05/ast_serialize-0.11.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:e5a9c53e64732118ae8235d0e57eb90e2333e90ff749a1a7dca6f8bfdfa5200e", size = 1483297, upload-time = "2026-09-09T16:04:50.136Z" }, + { url = "https://files.pythonhosted.org/packages/ed/48/ee13b4079ec67e9b333a87e5bb8eee643ab1b49f7173b3fca71158c6cdf6/ast_serialize-0.11.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:aecb606f67f21fd1c0ffba1826470d2ee400e41b23881ad9a1b2157306865b1a", size = 1501686, upload-time = "2026-09-09T16:04:51.734Z" }, + { url = "https://files.pythonhosted.org/packages/15/67/2175b79e1ee6b042e4bf8ed6871e60cafea7e9cf3fc69e87d7a9d520ae77/ast_serialize-0.11.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:d1ef9c478d8c8ca83499704d13e5ac32c840ed7ca7884aaaf716166aca5a2806", size = 1119522, upload-time = "2026-09-09T16:04:53.248Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c3/8c96aa5f121e9bdda0346b2df3919185eb1ec9a2cce2d20e311b4575ca0b/ast_serialize-0.11.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:27eef0739e0110f5db1ff5dfa38b3dadfcb6f0c31975a2b412f9f2bd72139cea", size = 1157260, upload-time = "2026-09-09T16:04:54.944Z" }, + { url = "https://files.pythonhosted.org/packages/10/bc/8aa663209e335eca73c9e1006197222b6b8dafa6665e513cdd975aa65496/ast_serialize-0.11.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:f9454960bbf185d33c669ccd5c9b76c96709a4542a95a65e27342c4d10dd8e20", size = 1132060, upload-time = "2026-09-09T16:04:56.501Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a7/e738887429de70350d9e25f84a95efbdd086b79579a811509dee8b02d7d5/ast_serialize-0.11.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:7b9a6089f1337838492b217707e9a55d0b9b407fe9169a960fa12282abf2234c", size = 1240585, upload-time = "2026-09-09T16:04:59.616Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f9/46b64fa883f3c8b8cf51629978f16d8a2da3d5c5c02f64add40864907703/ast_serialize-0.11.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:68929da7fb1c7375f69641baac9519d5c41ee441cd09a591ea5dfc83107ffe6f", size = 1228038, upload-time = "2026-09-09T16:05:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/ad/89/fe75c8d0f104a4be11cd0e23acecf129484c9ba06c7130033752d63a78c5/ast_serialize-0.11.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8945557ed3015173dbf8d44184da9621add9162720acbc0ac4832b5a091b4e8a", size = 1292388, upload-time = "2026-09-09T16:05:02.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/56/3f21c881900d8254a35195e1e962355b4d570ccd9cc193b0ba08abb81952/ast_serialize-0.11.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9cbed53bb8992b72e587dd47d0ca71703f5f715420f48ed57587c06c2fcd213", size = 1294413, upload-time = "2026-09-09T16:05:04.212Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b2/8d77be3dad1139158c59e370391f4990ea73f7f102983392272856bd3a63/ast_serialize-0.11.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65c836d1ab7af65e64a1dd73de82572566e838dd27529b1124eac0ac86f35e76", size = 1565921, upload-time = "2026-09-09T16:05:05.753Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/0675b5796c897f957fbdac6af0ebeeaa9a63cee866c1c54f1e5136ef5bf6/ast_serialize-0.11.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ab6df9898600ff45149b86c12bb5aa3359cc71843ad99ad0c924d11392469ab", size = 1312160, upload-time = "2026-09-09T16:05:07.269Z" }, + { url = "https://files.pythonhosted.org/packages/93/c2/1749fa4efae0dc98aad3c3d29f0178e2609208c3ba34700b4551082e40d5/ast_serialize-0.11.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3eebd3dd25a81839ac7d25b5f3fb3527f6141b35eca29f63d2613fe6254307fd", size = 1312405, upload-time = "2026-09-09T16:05:08.753Z" }, + { url = "https://files.pythonhosted.org/packages/00/14/2afc9f9d7db551f805b4d5e496946dc3dc2a703d27294ccc200b6ff07a7a/ast_serialize-0.11.1-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:311333c5f58f55fc04121be2c5adffa42a19e1038a43a2cef862fdf58c45bf4f", size = 1319458, upload-time = "2026-09-09T16:05:10.552Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4a/8b2866d9d6d5d0b037d7bf2b74db6e75a66695c503248632116f24009a8d/ast_serialize-0.11.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa8d0a9f6d02e7626cdf540555bc2f5313eabe803dcca04854fc628ddf4b1058", size = 1365055, upload-time = "2026-09-09T16:05:12.069Z" }, + { url = "https://files.pythonhosted.org/packages/47/17/73119504574f9b46610ffb718501b254c8adb727d4e43c5babc3c62d4529/ast_serialize-0.11.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e209a797fdf8680d58d2e969a9ddb25058ea682f6cff59dbabc98d8aef4e0db1", size = 1467771, upload-time = "2026-09-09T16:05:13.458Z" }, + { url = "https://files.pythonhosted.org/packages/56/f5/776bfb7a856ba0bb9bd373b76a90230fb77b1a10c987810b621ed244d5ba/ast_serialize-0.11.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:b391becd31e9889da438d388aac0362e3dfd222affbe45d920580c351486c787", size = 1571459, upload-time = "2026-09-09T16:05:15.15Z" }, + { url = "https://files.pythonhosted.org/packages/c7/43/86a655170ee36a8fdce65a922b7e34040bb319eb4ec251161032343c1fc3/ast_serialize-0.11.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:021afb3482d27d1dace9c8999724a13083e96c7ece785e53e418bf5df3b50e0a", size = 1568959, upload-time = "2026-09-09T16:05:16.677Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b1/015a3a156dc9bd46e9c4e4f24a939eb0b87179b1c53c9495ac6f088f13b4/ast_serialize-0.11.1-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:c1dc60251d93beff32d4147ecd4ccc518b0f022a900093f6f3021c2012416f0f", size = 1698173, upload-time = "2026-09-09T16:05:18.248Z" }, + { url = "https://files.pythonhosted.org/packages/0e/ea/a988b70980aca3a8a3fb731901dc911fe73a456cebbc225b767bbe5490ff/ast_serialize-0.11.1-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:b77d6a2267227d68d8221cfca177f8c6d9f68019f3250401d0355d2638db9fb9", size = 1493298, upload-time = "2026-09-09T16:05:19.806Z" }, + { url = "https://files.pythonhosted.org/packages/de/15/e049b2701ddab9087d7f62448f4c2c1b1463e676baa471345cf3e4a31e1b/ast_serialize-0.11.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e68ac7d7fd1a5d42a3a6d726d7cc8a7e0c9987934f11c6a5162aa2cf68e81e24", size = 1510213, upload-time = "2026-09-09T16:05:21.402Z" }, + { url = "https://files.pythonhosted.org/packages/46/a2/23d555eb842d1397dce54fac31851e6f151e03a0ae74db389d9b39718c33/ast_serialize-0.11.1-cp39-abi3-win32.whl", hash = "sha256:ed449d786b9032a7b85fcd6c2f7f54c4cd0e1e1ad254d396805ff922096bf08d", size = 1125239, upload-time = "2026-09-09T16:05:22.796Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4a/251f3fd1b8a5549edaedf8f3ba0b9fb5060e194d3c0ed208b593fccaeff1/ast_serialize-0.11.1-cp39-abi3-win_amd64.whl", hash = "sha256:6b43f5a9b9a8dd20ba3124914e63aa7d7427de761ac849081cda403c2112fda0", size = 1164260, upload-time = "2026-09-09T16:05:24.714Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b5/08e2f643feb9e4d72d42a484949ad51238b18262a01c7036196c201cc330/ast_serialize-0.11.1-cp39-abi3-win_arm64.whl", hash = "sha256:a579ea6f473aab3958a64734194b22fbaec108ec45d994c28d3bd721e1e1da32", size = 1137533, upload-time = "2026-09-09T16:05:26.095Z" }, ] [[package]] @@ -429,44 +429,44 @@ wheels = [ [[package]] name = "boto3" -version = "1.43.90" +version = "1.43.92" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/0b/53f833efea7331aab106a65f1156e0035beed9bacbfcb309b128204468d4/boto3-1.43.90.tar.gz", hash = "sha256:4b669742d5b45b8fd20ca50ac414a4e4cf995ebb8f280d21be28676e71c97594", size = 112689, upload-time = "2026-09-08T19:22:30.875Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/bf/322eace5751ada6198727ee9210b34be12670151dd9c001e36738272a41a/boto3-1.43.92.tar.gz", hash = "sha256:30a1ff4bb729831c4890e0a0d121f9d43f066de724c5916e358c9f68f94749dd", size = 112652, upload-time = "2026-09-10T19:21:48.52Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/c3/1d5c5ea7f599e2b80d122beeb2a9c1a8daff78325ea2391521e5fd5af1fd/boto3-1.43.90-py3-none-any.whl", hash = "sha256:aaaa1216d65ddb3dcf86bf9d93cfa436af05cbcd1e1f8b2847f2b83116012186", size = 140026, upload-time = "2026-09-08T19:22:28.555Z" }, + { url = "https://files.pythonhosted.org/packages/46/fa/51808a448896a4707321b6b325c21637f4e10f51beb3304525686693b8a7/boto3-1.43.92-py3-none-any.whl", hash = "sha256:aee16b1ad54caf6e7b8f48f2682d18b03a2feefaf7afaf56fed6204016fa687d", size = 140028, upload-time = "2026-09-10T19:21:46.45Z" }, ] [[package]] name = "botocore" -version = "1.43.90" +version = "1.43.92" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6b/3e/29872261075d878581a31e9e98c512c0b85322a36e6d6784b9f0694a918d/botocore-1.43.90.tar.gz", hash = "sha256:a139ed601e8b8fb1d730022355fe2b284b8c15cfe9ac0f100254a35d2273e1d3", size = 16081908, upload-time = "2026-09-08T19:22:25.645Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/cf/abc185dee932c8ebb3f35b608148226c1974e9c78af6b46a67901076dd36/botocore-1.43.92.tar.gz", hash = "sha256:a5efebb7d8fd9e7a47a1ac3138688dae4234fcb2f5355c51f7c7aced63ef6df8", size = 16096751, upload-time = "2026-09-10T19:21:43.374Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/b8/65cbd0d65553c3e51d27159de4c74308ea008c652b05ed3f830f32667efb/botocore-1.43.90-py3-none-any.whl", hash = "sha256:65f3394ef07314e45c92a90120988531d651136390d464a94938a92f665893f7", size = 15775313, upload-time = "2026-09-08T19:22:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/16/ea/84e86e2797afd7ce7e25c0da0b47c77fc636103659da17e2cc261e70da3a/botocore-1.43.92-py3-none-any.whl", hash = "sha256:a6f003615a3b6059628146e4a1d13bc132f7bc8f415c469a63d1c616bccee18f", size = 15788294, upload-time = "2026-09-10T19:21:39.287Z" }, ] [[package]] name = "build" -version = "1.6.0" +version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "os_name == 'nt' and sys_platform != 'darwin'" }, { name = "packaging" }, { name = "pyproject-hooks" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4d/b7/1db48a9ce2984842c8c886432ec8a2719613322e868a966ba82a28862f25/build-1.6.0.tar.gz", hash = "sha256:bd2c8afc603e7a2e0ce70e2ea85f0a6d02043bafbd307f5bada0f98669eca5af", size = 113825, upload-time = "2026-08-27T21:01:16.458Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/67/4898a44ea4f3f8e213b0954ec0aa0a16971d62a6212d6ea3931e97115b99/build-1.6.1.tar.gz", hash = "sha256:51cc11666391ab6f092070437ac747002ff46f3e4113a3622177ee6b488bfc53", size = 113427, upload-time = "2026-09-10T07:56:00.417Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/e5/aa1e81b21aea0ce0ba435311837a37d4cb936e7461f9fecac08580073ba9/build-1.6.0-py3-none-any.whl", hash = "sha256:f7aaf1ebbb79178a02ba248bb524f2176b256017e17e8e4bd4289c7b38cc2bad", size = 31187, upload-time = "2026-08-27T21:01:14.957Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9b/9fb3585dabcd73a1b2a6267f63f62649347c9e6d072c9fde365b105abb2c/build-1.6.1-py3-none-any.whl", hash = "sha256:ecd351a4be9d35a9eaaba244a7687143c9c7d4aea6ac964e7e7ddab20cbcf4e7", size = 31179, upload-time = "2026-09-10T07:55:59.148Z" }, ] [[package]] @@ -1120,15 +1120,15 @@ grpc = [ [[package]] name = "google-auth" -version = "2.57.1" +version = "2.58.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/3a/d3982b28d267880b5641f9a32c55d8062a9d2bad2d28d0274285391c89c2/google_auth-2.57.1.tar.gz", hash = "sha256:eb47b230fc6707eed4aee1c9cef55ec05bc1785eecba74ff8b572d531e921b1e", size = 372426, upload-time = "2026-09-04T00:50:27.593Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/ca/f398a483ce5aad18ca2f735646e45ccee2439bd94a41a4ad0cfa646bd495/google_auth-2.58.0.tar.gz", hash = "sha256:55e30cf15e737de92c5323d78cda8a83fcd57e7ffbaf900c4600039fd60a80fd", size = 380018, upload-time = "2026-09-09T20:49:38.043Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/27/0f7247b8002a1404fdb5412aeb15fb0fe70288eb8f88a5873d1c0d8262cf/google_auth-2.57.1-py3-none-any.whl", hash = "sha256:ab439dee60a6856412bc058f13a68eb6a59c5b81526bb89cfdcf89ce9c0a48c9", size = 259974, upload-time = "2026-09-04T00:50:21.693Z" }, + { url = "https://files.pythonhosted.org/packages/59/13/477d90d09591b3938b45c4e11f4d8a51291682112cb5efcac961e815d562/google_auth-2.58.0-py3-none-any.whl", hash = "sha256:8a9c4645bb4c8e91668fb1934b95ae6a8687084232753639220ba9bf04a1610d", size = 262404, upload-time = "2026-09-09T20:49:33.951Z" }, ] [package.optional-dependencies] @@ -1248,7 +1248,7 @@ wheels = [ [[package]] name = "google-genai" -version = "2.22.0" +version = "2.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1262,9 +1262,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/37/f1/f2f31b2a6bd826bc2cb73068df880954a026474c03a4315637d20cc13965/google_genai-2.22.0.tar.gz", hash = "sha256:9fa3b5d9ddb635005d8ab2d6206fb2b3d7204b66965bbce7de13ecd1a866ebcd", size = 684719, upload-time = "2026-09-02T18:06:02.906Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/9a/db14adffb0144584889e05f433a4e8ce540f9ed2928ed8b23cc65ab96178/google_genai-2.23.0.tar.gz", hash = "sha256:1ceebffdcd2af30c039a922ba05de61cd4054bb61c94be7649c4bf9ff8b33c5f", size = 686707, upload-time = "2026-09-10T22:55:31.12Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/96/0120d214958cb54f2b9c48da9f564a0e6eae269e9dd702415fb1d0551cc7/google_genai-2.22.0-py3-none-any.whl", hash = "sha256:c514001c45470cc0a942440ae1b8215445d12bfb6c373aac94637127e1f74ec6", size = 1088792, upload-time = "2026-09-02T18:06:00.629Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b0/f6bc58a6a7c8ad7ad2b6a36b0bf4732b3504dd7d48c6674f848304548869/google_genai-2.23.0-py3-none-any.whl", hash = "sha256:1e63211d44d188b8069c2b354d92b9bde25c1e821513fdbe1948b7c0d9f6b922", size = 1093785, upload-time = "2026-09-10T22:55:29.282Z" }, ] [[package]] @@ -1495,7 +1495,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.30.0" +version = "1.31.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -1508,9 +1508,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/35/97/2eb4abaa5b969ed385066a0496a3823b3ff467fc1082e2202955f1867d60/huggingface_hub-1.30.0.tar.gz", hash = "sha256:e6a6120bc8c8e2723d03648434ee247088cceb55ba7067e7d34d692cad5fdb57", size = 964291, upload-time = "2026-09-03T10:05:14.053Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/f0/61159db90b5cd275d55516fe27920828e7d3be4053fdbdb27c3f70e5f1ef/huggingface_hub-1.31.0.tar.gz", hash = "sha256:f8e9e710a210613fa5d0f26bba6da05ef4aef9fba5a0f23f508f5ac4d08b6f90", size = 968039, upload-time = "2026-09-10T10:27:22.724Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/0e/3e45bbe0dd48f4e56b1d46649d342de853cd1c7e815323472ab62687f153/huggingface_hub-1.30.0-py3-none-any.whl", hash = "sha256:96ae0a8e99a234374a6fe43e989ebd21c04640b91ab2927e7e5773ba1131ca59", size = 796795, upload-time = "2026-09-03T10:05:12.21Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7f/3f886a625043b77312b80da2f2bf00b5ecbf5a73061af1aa0259cd258c9d/huggingface_hub-1.31.0-py3-none-any.whl", hash = "sha256:9dbb6a503cbe2494ea666695207e7262d410659e09134059deb83e5480864667", size = 798313, upload-time = "2026-09-10T10:27:20.798Z" }, ] [[package]] @@ -2065,7 +2065,7 @@ wheels = [ [[package]] name = "litellm" -version = "1.100.0" +version = "1.100.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -2083,15 +2083,15 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/03/ce/1e1ce2558f65244057c0c40c60ade4dfa3767da3522bf1dd8679507ad7ba/litellm-1.100.0.tar.gz", hash = "sha256:ece94e817a453a5b3a9517c03547c428d501cea719edb728c1b260e53f78ea35", size = 17287857, upload-time = "2026-09-06T00:22:46.023Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/a5/f78d2fafa950040f833efd41dd5690598afeef0c9c4657171372573698aa/litellm-1.100.1.tar.gz", hash = "sha256:d24b5fbdeb1f0b5c0a6f7f0caf7b6aa79b69c704b90daca57e3ce7d50a9d6bf4", size = 17330008, upload-time = "2026-09-10T01:42:53.328Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/92/983d15efffd9bab37ebbff255fb09417814ecd959bd2a2459b8afb6c205d/litellm-1.100.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:098a413e398e2220734cf9c8dd75fb34a38b65b64090619c2869af1fdeaf4ae5", size = 23899430, upload-time = "2026-09-06T00:22:21.941Z" }, - { url = "https://files.pythonhosted.org/packages/59/e1/ea3f868ad6c84b2232f41d5d56adb5f0794ebb1cf32fc0e78effa5842170/litellm-1.100.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:0f87fae695edbca27e5cf970bea52fa405fa9910fdf7c29c963d6413db767299", size = 23552365, upload-time = "2026-09-06T00:22:25.456Z" }, - { url = "https://files.pythonhosted.org/packages/61/50/9439bd3238c9d8c7efcd5165555f175cc8e99e748ba99e8cc7e502787265/litellm-1.100.0-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a07370d116905485e9ac99679bac991a0a6c81e13c99ca3f007128fbdf2b0082", size = 23693340, upload-time = "2026-09-06T00:22:28.549Z" }, - { url = "https://files.pythonhosted.org/packages/e9/05/34a1df3a5fe1db92bf43d3b25af2e5a9c36c54ab6813acef0c31cce2b4e0/litellm-1.100.0-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:8224c8eed9cab3319a88e6665d1275ad8faf21d353b1b22223a6d6115a302ea2", size = 24063361, upload-time = "2026-09-06T00:22:31.913Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d0/4c7e8c8402e5af8c9f55602484c2bc888f0a8fe5c2a01df2a3705ba2139b/litellm-1.100.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e3787fb7ad1f20aebdde7686a85061880bba6550c9d62bfa1cf8df089fe7899b", size = 23766707, upload-time = "2026-09-06T00:22:35.06Z" }, - { url = "https://files.pythonhosted.org/packages/4e/a2/81595fcda1457b777739d265b9383c1061c884af37111223b5fecbf72588/litellm-1.100.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:0b7ec93013e18535481cd811b776ee95c6b957b3a9fb44dc53f7c459e7e60e38", size = 24163673, upload-time = "2026-09-06T00:22:38.418Z" }, - { url = "https://files.pythonhosted.org/packages/ce/96/1ee7a3c86c4e9afec528d094b30158cd55b8bbc619e1632ad897bbd1291a/litellm-1.100.0-cp310-abi3-win_amd64.whl", hash = "sha256:c6f2f56808d05d8d2a7766129d958101eafb1a47e30ba5ddcfa900cdbc50af67", size = 23974360, upload-time = "2026-09-06T00:22:42.783Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b7/04e5f938a1c3aa4db5a77287571f700e5e217199c1524ebc991933537c49/litellm-1.100.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4e2d410538c8a0d7b58440c47d7e1a6120c3219dfb5d100f39069fa8dbce0055", size = 23899191, upload-time = "2026-09-10T01:42:29.375Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/3105d3a3778875139bf2ba71eae0b37c83a269beca7433a3b0bef9cdd9ed/litellm-1.100.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:1d2e622a2e2eab74efd25ed356208568b783cb406f413efd083e3416b81d1606", size = 23552125, upload-time = "2026-09-10T01:42:33.277Z" }, + { url = "https://files.pythonhosted.org/packages/2d/8d/1417d7bd3708789f8b5b9334ea996461b119615cd9de4c0dce881354e095/litellm-1.100.1-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f24ab714b58a66276623b48ed58c0fbf4e1ccd6fd127b187a049ce0c74eba8ab", size = 23693099, upload-time = "2026-09-10T01:42:36.241Z" }, + { url = "https://files.pythonhosted.org/packages/1c/76/e087de4ea74635a790e2d8e65870409eae872a3d2dc9773a65b9d0559a35/litellm-1.100.1-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:8f53d917ece795e35c125ec24d342ddc2103a806c22b1f7b1517110d31f45362", size = 24063121, upload-time = "2026-09-10T01:42:39.437Z" }, + { url = "https://files.pythonhosted.org/packages/b5/8b/28f44f6fb841451fff57f66504283316628a80c13359b03819ea6c1967e9/litellm-1.100.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:63201b3ed92cbe6ff9ec26ee83436efbd67c5f6d38b2d1ece0203f04819fc4c3", size = 23766467, upload-time = "2026-09-10T01:42:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/5b/9a/095fd6051c80923f883888aa90ffdcfb15faa958124d6b8bf134c122d5fa/litellm-1.100.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f150a169807a1b24c0ffc5bd1838f77659bf7449db8718e65d1be1bca517825f", size = 24163432, upload-time = "2026-09-10T01:42:46.28Z" }, + { url = "https://files.pythonhosted.org/packages/61/78/b1b701cd7342eea1e39aacb7c04e0971089bf9ede2b7b6788546bed25ced/litellm-1.100.1-cp310-abi3-win_amd64.whl", hash = "sha256:2f45760e61a624660444d110ae6475edc959e2906914c6c308c710c8be2ca742", size = 23974164, upload-time = "2026-09-10T01:42:49.789Z" }, ] [[package]] @@ -2321,65 +2321,54 @@ wheels = [ [[package]] name = "multidict" -version = "6.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, - { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, - { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, - { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, - { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, - { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, - { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, - { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, - { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, - { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, - { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, - { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, - { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, - { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, - { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, - { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, - { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, - { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, - { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, - { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, - { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, - { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, - { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, - { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, - { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, - { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, - { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, - { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, - { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, - { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, - { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, - { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, - { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, - { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, - { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, - { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, - { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, - { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, - { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, - { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, - { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, - { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, - { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, - { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, - { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, - { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, - { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +version = "6.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/95/989c1b5ca17b72128661530cd6e351a0a83cda9a4d6c036e9ed976c18931/multidict-6.8.0.tar.gz", hash = "sha256:5cd4637ce76312ba1e05eb9c5193fec231f64fee0944e135fa1e951242355b37", size = 122412, upload-time = "2026-09-09T13:57:57.967Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/83/a4621577679149ea001806f5963f3fc687c391c1bd5217157be2278863f5/multidict-6.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:22a310ad37672a261e55a8b5e28d0ae08cfb68abb1f46418ccd19835c3b8e836", size = 84146, upload-time = "2026-09-09T13:53:49.163Z" }, + { url = "https://files.pythonhosted.org/packages/09/00/236b063f3e606055a3a9ba8faa5d40e6c688b059a58056b055f213476f46/multidict-6.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bb8c7da8c861391f7ae48e3593762be2dabe405109e01aec520fbe1a6d15d14b", size = 51049, upload-time = "2026-09-09T13:53:50.46Z" }, + { url = "https://files.pythonhosted.org/packages/91/9d/954b139bfa969855f2d4cb5ae7b7d44dd7106f754305b6e21a9068213aa7/multidict-6.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10456943903744ae1249728161c96bd9d2f7eb5ee17fcc2ffda2dc32e1bb36c7", size = 49362, upload-time = "2026-09-09T13:53:51.878Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8a/8774f5b3f6d5266ecd1117876e04b405f0f1ce19aa750b35a826efe6cfe4/multidict-6.8.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:658f5a1895b804423d97b22d06fc0d0b171c7c01dcc3aa9c8faf0c0e26a249a5", size = 278619, upload-time = "2026-09-09T13:53:53.44Z" }, + { url = "https://files.pythonhosted.org/packages/db/47/736080fec911ed9f2dd57ccab5a8145e4f17c4987de0bfc27bee20e4d170/multidict-6.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c10b22860dbd09982d0b8993b66231a861bea2993d4a817ff35273f6ea285a", size = 283771, upload-time = "2026-09-09T13:53:55.048Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d5/b7f41f59b0583f092602308a5e7c16ec5efd00d60214b22511e89a38dd19/multidict-6.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:088b04a66b3c1fce6fe4d771ec184a0426262d0b86709c908477b4ac7965df40", size = 262108, upload-time = "2026-09-09T13:53:56.631Z" }, + { url = "https://files.pythonhosted.org/packages/54/b2/a52dc06c6e2598672308e3d392fd85b837b23c25dda459bedaea84985080/multidict-6.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9caef53b20a105c0d66518a34be2f71b2783de8d091767575ef86f6ea422236d", size = 289899, upload-time = "2026-09-09T13:53:58.415Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/00cda7983f37d119b86f1f89d5b4cf771ecb6d0fedeb9a0971758d6d6d4a/multidict-6.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a5e1583c14775580da05641240ce0d93f36ce3ddef3d5083a827468b0bcfe874", size = 293025, upload-time = "2026-09-09T13:53:59.973Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c7/4544cc02e45bbfac4d8788b05379bb360021fd8c53fa74b0f624126ac188/multidict-6.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:003a3bddb32915c3f67096ea41d24e53edf710edb65a1f5d0c70ab40b0e4d20b", size = 287410, upload-time = "2026-09-09T13:54:01.652Z" }, + { url = "https://files.pythonhosted.org/packages/43/1a/7abed90b8eba381842235bfa6f4d730204fd7deb374fc87e3ec9b2c2b4ac/multidict-6.8.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35977263d9bf506dbc65349f63b3b8c91606d4abc110990945e3b94bc671319c", size = 255878, upload-time = "2026-09-09T13:54:03.366Z" }, + { url = "https://files.pythonhosted.org/packages/25/3e/73fae10e15fc4d711975337caff7e494c87de5d0189afe3518b21b945326/multidict-6.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9dc7b4ff6ef184504b49ef9a4113d49a646653b2ce89f5f48c1f57cdf6ba081", size = 277831, upload-time = "2026-09-09T13:54:04.963Z" }, + { url = "https://files.pythonhosted.org/packages/c5/cf/01cfc81492933331147004861bdff201d8adeba8485ecd8f490e755fe7e8/multidict-6.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:887f9a975996032c686719eb7b3e1e7942fab5079c2b778bbd9afe9a9d78244f", size = 275096, upload-time = "2026-09-09T13:54:06.661Z" }, + { url = "https://files.pythonhosted.org/packages/de/59/e9a3773b17297fa1e38fd4b3c6f5f2f458380796be62eca7d0d77c250618/multidict-6.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f3071e6515cc63714d014da8f738ae9fa3997c476203f3cd46de380c2376ed7b", size = 279803, upload-time = "2026-09-09T13:54:08.389Z" }, + { url = "https://files.pythonhosted.org/packages/64/9d/2d712a2605b3971908e3b4f5eb6f98c353d9991e106f684d0e08ae581814/multidict-6.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5f3a2af441670d80ce5fdf13b6c1b421fc1fc7fc5182d58ac7486738bb2b742", size = 284595, upload-time = "2026-09-09T13:54:10.17Z" }, + { url = "https://files.pythonhosted.org/packages/58/6c/21aded8586e552b29892268c576e5745d1a894c5451c9866ca3c06b7ec50/multidict-6.8.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:31e8901637e20ccb3cf8f8848b5d0f7a00462bf5b34f7cf3dcbb2753b18e8b39", size = 252641, upload-time = "2026-09-09T13:54:11.811Z" }, + { url = "https://files.pythonhosted.org/packages/2a/70/56a415ae0a45e5eae2ec817d46aeb72a1ae777863621c85f1f39d329275b/multidict-6.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:13967dca8b2f33230a1427b52438326bb1c9101a1df22a3309ed3fcbbb3c96f0", size = 283369, upload-time = "2026-09-09T13:54:13.59Z" }, + { url = "https://files.pythonhosted.org/packages/08/7e/7b7cd611fd94bf2f6bd16244c50495867ba394d5baaf8e6e487d39494ab3/multidict-6.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad474c11d851b6fc97cb625e4822bc0cbd567fc07dc2602e28faec5a36b42bbb", size = 281653, upload-time = "2026-09-09T13:54:15.174Z" }, + { url = "https://files.pythonhosted.org/packages/33/4a/b19a5892ef2ef6c68ae278b4f1504b82e01037baedd92c55d37e55ecad00/multidict-6.8.0-cp312-cp312-win32.whl", hash = "sha256:7bb0dad75068fee80fcb60f88569722c199d8656a16706702dc6e3b786819c90", size = 47936, upload-time = "2026-09-09T13:54:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/29/00/1952f9f282aa71e7c3db3a6b47afb689d0ddf283dbded7e6326a91d421c9/multidict-6.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:7d26dc8f070c0ec5579e987fa615ffd6883086106eefdff9e10d160fc5630630", size = 51723, upload-time = "2026-09-09T13:54:18.05Z" }, + { url = "https://files.pythonhosted.org/packages/49/b5/c9d57dbafe25b8f3460ce2961c968539a81ff7a70160c44dcfd4255cbcd1/multidict-6.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:e6ec7d37841609a691b96a10b4fde386c7cd93ebbb939f59c9f23325ee788395", size = 48492, upload-time = "2026-09-09T13:54:19.42Z" }, + { url = "https://files.pythonhosted.org/packages/84/1f/d7112c2dd7db02677097be72fb65542f51a5aa73cb472b87ec211ba9e0dd/multidict-6.8.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:ec0a4d066356054d569a66e0a94691a2058b680be5e710298f61db11a3c4609f", size = 54197, upload-time = "2026-09-09T13:54:20.814Z" }, + { url = "https://files.pythonhosted.org/packages/ae/24/876015abbcb4a179d946579eb77b778eb5a948fc8381bc7928ba895bc051/multidict-6.8.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:714597cb5d5e15a8a449d2ae23c45b486a9e8fa33c462c7a33d7f35b65d92943", size = 47787, upload-time = "2026-09-09T13:54:22.51Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/ceb7d25f8a567599db2eb19b08cac58d67ff553cff42dcadbea9aba56a20/multidict-6.8.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e0db3a4d1e264e225037a6023888972c25206a96e016021a5bea41c9a939f2a9", size = 48815, upload-time = "2026-09-09T13:54:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/18/e3/e1c6e9c3818c34b782f23ce5fdba3eaa34ec6750dc53078dfac80fa59be7/multidict-6.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:27747162712e85c84598d364425dbf1714ff335bdb6ba3171c4e5081196e8916", size = 83484, upload-time = "2026-09-09T13:54:25.674Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a0/c23f78a4badee9a5b3e760495c661c62a92c340a1dfd00f829cd16e256bb/multidict-6.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:847d6082ae694dc95e548acb201bc100e1cfa96513bc71fdcb86f709dad6c435", size = 50763, upload-time = "2026-09-09T13:54:27.135Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/db552d402a3f6b650f5d3ae11b82b93833836aebb51bcda22d8691121129/multidict-6.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:88a6df88567680504ae28bfa7a1f2f64243d91e79a40b2c92ef42efc531e23da", size = 49029, upload-time = "2026-09-09T13:54:28.483Z" }, + { url = "https://files.pythonhosted.org/packages/01/b4/546853fba19dcef77cdf91fc173faf0b02284a49106cf250511166b4ec5c/multidict-6.8.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:560b211fc3bd4a1e1c6de44f6d38113bf5b410dfc89a4c0d2a3c0edbf1a0dfb8", size = 278863, upload-time = "2026-09-09T13:54:30.145Z" }, + { url = "https://files.pythonhosted.org/packages/ee/3f/4b52dac7db547936eb762123ac1d99df23f92fdb358bae600e322f611247/multidict-6.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202436df907c15adbb94360296c425ea53cf8968a5d2cff9b5b9790ae1972b33", size = 283915, upload-time = "2026-09-09T13:54:31.937Z" }, + { url = "https://files.pythonhosted.org/packages/fd/6e/c0dfbf170e49a91bcb9ce850d51cb98357f3033c5227529200ca7625853e/multidict-6.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c46a08bf070d6849fed483e9d9833f9d06aecb8382ed985be0b38508b3ae958e", size = 260704, upload-time = "2026-09-09T13:54:33.529Z" }, + { url = "https://files.pythonhosted.org/packages/91/02/56973a060ab8dfc2e80bb6797682f6577aff7123cdb1de1a568670ae3499/multidict-6.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cd560498ae8e1bcc955643c1d78eb8e338226d07a983c656ea8c4443d3eec0f", size = 290243, upload-time = "2026-09-09T13:54:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d5/67/69112989f131bdea4a87b74e82cb0a2daf37880cd92b0e6f0420020adceb/multidict-6.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:758233648ac47b07c575224c4eadd73c8929c3b4c31e2afcfea935fde1cda735", size = 291131, upload-time = "2026-09-09T13:54:37.205Z" }, + { url = "https://files.pythonhosted.org/packages/c2/75/9435f68b0cfc442d4917de85c26f2b2e1292630883414a25576083fa2469/multidict-6.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:122adc7c46ac1e31ecfc7f81b2530533dccafdba70f5d741649f87e336c63384", size = 287551, upload-time = "2026-09-09T13:54:38.835Z" }, + { url = "https://files.pythonhosted.org/packages/13/08/2ee4838081d6587849611aa7ec722c4cb2469e912fd0eaee980e7bac064c/multidict-6.8.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8125e60f3c70e323ac07dd8b3635f7b3bbc5c3a9ac04ae5988f668ff7ae28a18", size = 254591, upload-time = "2026-09-09T13:54:40.806Z" }, + { url = "https://files.pythonhosted.org/packages/94/f1/05673b51191f77f4198b8e4b35f16ea71c0300c72ca8aa027a66a61b6edc/multidict-6.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:83ff054b04915be5c15680da6c6012474a2cc2bf534129a0e8c6a99f17ba7238", size = 278204, upload-time = "2026-09-09T13:54:42.672Z" }, + { url = "https://files.pythonhosted.org/packages/45/4f/b6cf74322b3fbd3e011a1e903730191922291a7779f6d404114c2189b806/multidict-6.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:930c6058047410e3edff445f5a6e4457f2e089042dede00e2d18ce06f3ceae2e", size = 275600, upload-time = "2026-09-09T13:54:44.348Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ab/958bbb04377159ff03c7314cd9d8a48dd6fc4f78c840589c22ab155ee9c7/multidict-6.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:13e26f59f0eecfc5f67c663ad550ffdaf62c0f657547cde387f6c86af1c9449e", size = 279793, upload-time = "2026-09-09T13:54:46.086Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3a/706605ab0dfc4179748ee7949829e63c6f14ae28667aceeefaf2c701807f/multidict-6.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd789a294d8e098528be29b2669b83005ce569339f8cef167fc0274c3115c34c", size = 284751, upload-time = "2026-09-09T13:54:47.793Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a5/567e36c013ad023546de633079c6b22101dd43226b193cba00e6399703be/multidict-6.8.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:3126f2a96704505aa4e92a72d6e8a5d7f29d40a987ced8bf69e29d71dfc71fbc", size = 250812, upload-time = "2026-09-09T13:54:49.509Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5f/6b0b64aa0cd346b07831dabaa6ccda0e73014c5df044b68baa763f0f0552/multidict-6.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:23c9ee89967b6a9b4048acb3b93b660ed714ce9c8bf3bbe652959bc120dc02dc", size = 281606, upload-time = "2026-09-09T13:54:51.288Z" }, + { url = "https://files.pythonhosted.org/packages/31/8c/b846b6796f26d496efb07fedef2b69f6de533da32a56f12d236722a96157/multidict-6.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a62e302fc8cd6aa8972207e7e951d1fdee7c1dda18568305041d19f0e2c00f5", size = 281733, upload-time = "2026-09-09T13:54:53.05Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f3/bf14a39d4af5697fd9404baaf70a0aeeb82d258b95de5cb16b1a7f98ae6f/multidict-6.8.0-cp313-cp313-win32.whl", hash = "sha256:093167d22a8c95af30f597b8a5686f20a14512989942d4be804d119899caca20", size = 47738, upload-time = "2026-09-09T13:54:54.676Z" }, + { url = "https://files.pythonhosted.org/packages/19/0a/598511a5741a3cb374971b3b02eda8a09896118ba528a54795f7e7e8bfb4/multidict-6.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:f25b61a708bd276e8cbb6afcbbf1b8e793a3be70ba0a842d0b8692020f83b706", size = 51609, upload-time = "2026-09-09T13:54:56.38Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b7/6f5c1bd4ffe42d4a6db0f2f65491d4088e9c25c990358fb31a614621d664/multidict-6.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb36381e1f9f9d06eba2f10bdd438e5d20c07d5b55e1a3eee30b9f44cbf52316", size = 48280, upload-time = "2026-09-09T13:54:58.03Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ee/be4e1a4b7a2b27f4fb6936510d4bebcb41b0562c946930ad26916e069cf9/multidict-6.8.0-py3-none-any.whl", hash = "sha256:75daa15ca16d6285eb2e104b2f05ee6f8d9836c68da3ce5c85f615a0450eed0e", size = 16297, upload-time = "2026-09-09T13:57:56.106Z" }, ] [[package]] @@ -3164,32 +3153,32 @@ wheels = [ [[package]] name = "psycopg2-binary" -version = "2.9.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/9f/ef4ef3c8e15083df90ca35265cfd1a081a2f0cc07bb229c6314c6af817f4/psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", size = 3712459, upload-time = "2026-04-20T23:34:30.549Z" }, - { url = "https://files.pythonhosted.org/packages/b5/01/3dd14e46ba48c1e1a6ec58ee599fa1b5efa00c246d5046cd903d0eeb1af1/psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", size = 3822936, upload-time = "2026-04-20T23:34:32.77Z" }, - { url = "https://files.pythonhosted.org/packages/a6/f7/0640e4901119d8a9f7a1784b927f494e2198e213ceb593753d1f2c8b1b30/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", size = 4578676, upload-time = "2026-04-20T23:34:35.18Z" }, - { url = "https://files.pythonhosted.org/packages/b0/55/44df3965b5f297c50cc0b1b594a31c67d6127a9d133045b8a66611b14dfb/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", size = 4274917, upload-time = "2026-04-20T23:34:37.982Z" }, - { url = "https://files.pythonhosted.org/packages/b0/4b/74535248b1eac0c9336862e8617c765ac94dac76f9e25d7c4a79588c8907/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", size = 5894843, upload-time = "2026-04-20T23:34:40.856Z" }, - { url = "https://files.pythonhosted.org/packages/f2/ba/f1bf8d2ae71868ad800b661099086ee52bc0f8d9f05be1acd8ebb06757cc/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", size = 4110556, upload-time = "2026-04-20T23:34:44.016Z" }, - { url = "https://files.pythonhosted.org/packages/45/46/c15706c338403b7c420bcc0c2905aad116cc064545686d8bf85f1999ea00/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", size = 3655714, upload-time = "2026-04-20T23:34:46.233Z" }, - { url = "https://files.pythonhosted.org/packages/b3/7c/a2d5dc09b64a4564db242a0fe418fde7d33f6f8259dd2c5b9d7def00fb5a/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", size = 3301154, upload-time = "2026-04-20T23:34:49.528Z" }, - { url = "https://files.pythonhosted.org/packages/c0/e8/cc8c9a4ce71461f9ec548d38cadc41dc184b34c73e6455450775a9334ccd/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", size = 3048882, upload-time = "2026-04-20T23:34:51.86Z" }, - { url = "https://files.pythonhosted.org/packages/19/6a/31e2296bc0787c5ab75d3d118e40b239db8151b5192b90b77c72bc9256e9/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", size = 3351298, upload-time = "2026-04-20T23:34:54.124Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a8/75f4e3e11203b590150abed2cf7794b9c9c9f7eceddae955191138b44dde/psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", size = 2757230, upload-time = "2026-04-20T23:34:56.242Z" }, - { url = "https://files.pythonhosted.org/packages/91/bb/4608c96f970f6e0c56572e87027ef4404f709382a3503e9934526d7ba051/psycopg2_binary-2.9.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", size = 3712419, upload-time = "2026-04-20T23:34:58.754Z" }, - { url = "https://files.pythonhosted.org/packages/5e/af/48f76af9d50d61cf390f8cd657b503168b089e2e9298e48465d029fcc713/psycopg2_binary-2.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", size = 3822990, upload-time = "2026-04-20T23:35:00.821Z" }, - { url = "https://files.pythonhosted.org/packages/7a/df/aba0f99397cd811d32e06fc0cc781f1f3ce98bc0e729cb423925085d781a/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", size = 4578696, upload-time = "2026-04-20T23:35:03.409Z" }, - { url = "https://files.pythonhosted.org/packages/95/9c/eaa74021ac4e4d5c2f83d82fc6615a63f4fe6c94dc4e94c3990427053f67/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", size = 4274982, upload-time = "2026-04-20T23:35:05.583Z" }, - { url = "https://files.pythonhosted.org/packages/35/ed/c25deff98bd26187ba48b3b250a3ffc3037c46c5b89362534a15d200e0db/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", size = 5894867, upload-time = "2026-04-20T23:35:07.902Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/8d0e21ca77373c6c9589e5c4528f6e8f0c08c62cafc76fb0bddb7a2cee22/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", size = 4110578, upload-time = "2026-04-20T23:35:10.149Z" }, - { url = "https://files.pythonhosted.org/packages/00/fc/f481e2435bd8f742d0123309174aae4165160ad3ef17c1b99c3622c241d2/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", size = 3655816, upload-time = "2026-04-20T23:35:12.56Z" }, - { url = "https://files.pythonhosted.org/packages/53/79/b9f46466bdbe9f239c96cde8be33c1aace4842f06013b47b730dc9759187/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", size = 3301307, upload-time = "2026-04-20T23:35:15.029Z" }, - { url = "https://files.pythonhosted.org/packages/3f/19/7dc003b32fe35024df89b658104f7c8538a8b2dcbde7a4e746ce929742e7/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", size = 3048968, upload-time = "2026-04-20T23:35:16.757Z" }, - { url = "https://files.pythonhosted.org/packages/91/58/2dbd7db5c604d45f4950d988506aae672a14126ec22998ced5021cbb76bb/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", size = 3351369, upload-time = "2026-04-20T23:35:18.933Z" }, - { url = "https://files.pythonhosted.org/packages/42/ee/dee8dcaad07f735824de3d6563bc67119fa6c28257b17977a8d624f02fab/psycopg2_binary-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", size = 2757347, upload-time = "2026-04-20T23:35:21.283Z" }, +version = "2.9.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/76/7b4383014be0fcc6c1c0e24292845a14e1672cf17fca62ca0a2bd5f4563d/psycopg2_binary-2.9.13.tar.gz", hash = "sha256:e324ecf60f952d21dd11413b8bbed0951bbd99579a06fd06f28bfc37737cd373", size = 378112, upload-time = "2026-09-10T00:06:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/d1/d0125c56b865e3bc9f318d84930b2df71a729229dbb0ce12de748a82a6d7/psycopg2_binary-2.9.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2bf9f97a6df69a5d89d054b8cf5257a0916096c479800715fbfe7974dbcb3a26", size = 3725735, upload-time = "2026-09-09T23:54:51.182Z" }, + { url = "https://files.pythonhosted.org/packages/54/a5/b5a73d0910555e38ee12c49c1740855f8a1e9776e87d65f0c51e1bab762a/psycopg2_binary-2.9.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:07b7bd9f410650c34c3532162cc329f112368d78a3fc8668cb1ea9df61bc11bf", size = 3818000, upload-time = "2026-09-09T23:54:53.229Z" }, + { url = "https://files.pythonhosted.org/packages/3d/43/3e4783f62ae3f4fc19a5acf8d1c394df54458f1336febe188f317556d2a7/psycopg2_binary-2.9.13-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0463c00f946517f3e69192a59e6601e023ff9de45ad0a875eda3d6b1bebeb7ce", size = 4585760, upload-time = "2026-09-09T23:54:55.313Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c4/a9a67ae65ad3d567eb0fc9cdf9a5a2783b779aecdcdc8945f1807b13d99e/psycopg2_binary-2.9.13-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e3861eba31f8ea8663fd876166b032fd89179e42aa63764d6feb281f13f9eb60", size = 4282380, upload-time = "2026-09-09T23:54:57.362Z" }, + { url = "https://files.pythonhosted.org/packages/b3/db/9d459d3da12e0b841cf1596579455aaa27e9e593e3e9a5a4ded5a55a7c15/psycopg2_binary-2.9.13-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3dc3372b3731b3ef23407fe06b94f640ef87a2bda242fa386033d5589c87514a", size = 5902039, upload-time = "2026-09-09T23:55:01.955Z" }, + { url = "https://files.pythonhosted.org/packages/d6/53/21079c10a581c50b6817498eda7c3481c1b3cdb41482bd08ebfccd3664c4/psycopg2_binary-2.9.13-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0405dd4d97720e7ab177aa02e493f524907c4cb3c445ac173e2627948d3d0528", size = 4119840, upload-time = "2026-09-09T23:55:04.336Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ce/71e8d9e1b4f3e78157b49a5abdff50d915e95f2812550f6c9b4f2e4d5e94/psycopg2_binary-2.9.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b6ae51708201f501a171b02419d0c30878a743c369c9054eb1289f0f8d5979e2", size = 3661077, upload-time = "2026-09-09T23:55:06.118Z" }, + { url = "https://files.pythonhosted.org/packages/d9/54/b17616472f09a0fae96f8852692948b7eaa7c971d7629696da0e5932d996/psycopg2_binary-2.9.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:81682c227cc1849c4a6adf7b85274229073bb4c9d6ad5697222c695dcea5a8a7", size = 3307202, upload-time = "2026-09-09T23:55:08.061Z" }, + { url = "https://files.pythonhosted.org/packages/77/c7/d9737e222a377dac67a0ce0a2c73e7231a57f5cf18bb35a65d5c8d45d5d2/psycopg2_binary-2.9.13-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:13d955f6054a705a19554364fe9888d0a6e8b0746dc7ebc08a447c7b4fd4145c", size = 3052893, upload-time = "2026-09-09T23:55:10.209Z" }, + { url = "https://files.pythonhosted.org/packages/7d/3d/c406c9f698f518c264381192c2bdf8952ee84e469ffa9f82db1411f57385/psycopg2_binary-2.9.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e2405196a8cfe6cd3e54172a54452dcf85c241eaf2e9dde7190d7469f7f5ef7", size = 3355210, upload-time = "2026-09-09T23:55:11.883Z" }, + { url = "https://files.pythonhosted.org/packages/27/64/6e3a96699770af2d0d49a2002f722c69b656fc27623ff89281cf2b109644/psycopg2_binary-2.9.13-cp312-cp312-win_amd64.whl", hash = "sha256:376ebf7d8aee4b7386b2bac31fdc27911e7e57cd0a88f1e038b8b149398ac008", size = 2767784, upload-time = "2026-09-09T23:55:13.823Z" }, + { url = "https://files.pythonhosted.org/packages/82/0a/795f2869788373cf7d08410341a444196e8ccebbac07a70a8f9a1f60e72f/psycopg2_binary-2.9.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4d66bfd44a46eb88cff0287929a4193fb45166b6c1f84bb1b233cc17ece0813c", size = 3725723, upload-time = "2026-09-09T23:55:15.887Z" }, + { url = "https://files.pythonhosted.org/packages/b5/63/5a9633f4563a73beba69b20a846ddd14c1c6ac072f5e8aab0da97ffabc2a/psycopg2_binary-2.9.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f818161d2302b3b3e9c75d5a1d0a5c5679e92e45cfec6432b9d5432dde5ff1f1", size = 3817976, upload-time = "2026-09-09T23:55:18.025Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e2/b2e3b3a4331dc8b58e328cda30f3d0cc43a94b7aaf0c8383efd53dd10e95/psycopg2_binary-2.9.13-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:31db6cba66df5231dfd91d9f69188bec3fe6c8baae384e93a0ce792067ee2d98", size = 4585813, upload-time = "2026-09-09T23:55:20.112Z" }, + { url = "https://files.pythonhosted.org/packages/56/5c/87daea77c4132114d1a5da3a4928dd59446c3b3cc73d288cae08cf0b91a6/psycopg2_binary-2.9.13-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f04ada42bcd537adbaf8b7f3140237a204e452a88d0c1831cfce69f7d2e59f4e", size = 4282438, upload-time = "2026-09-09T23:55:22.329Z" }, + { url = "https://files.pythonhosted.org/packages/91/e5/56f9efdc9337acbd1a75798d97163183b63a1babc17602f7163009506c96/psycopg2_binary-2.9.13-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa37089795bd9701576edc2eb5849ce77a439eda9dfdfa47857449332cfa5292", size = 5902064, upload-time = "2026-09-09T23:55:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e4/15/f7ed0b90b47b73a9087306b42267eccfd919f92c0fb057e46bd2fa2efa4d/psycopg2_binary-2.9.13-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:41c2eb569ebd0e1b02d30d361a46932923b193fe1b5e641fb4d547c75e218955", size = 4119848, upload-time = "2026-09-09T23:55:26.433Z" }, + { url = "https://files.pythonhosted.org/packages/42/08/3091347b9fc5766e979aba6b0756ad14ce867a6bb245f3d69ac71fb768c6/psycopg2_binary-2.9.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f699a5225094a5c61402984e2fc1eca20e940223e76767c88189efb0c313f69", size = 3661129, upload-time = "2026-09-09T23:55:28.449Z" }, + { url = "https://files.pythonhosted.org/packages/34/c4/4f9a84d55484c9794b364548eb6e1fe10a57f123afd19729e5a1cc8ad7fc/psycopg2_binary-2.9.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5f04ae99c9fbb94c3197ec88599ed7db921f6adcddfe83687a74c7ead4037c22", size = 3307262, upload-time = "2026-09-09T23:55:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/83/42/6eba8306a61dc890805ae475a9e71790a1c5461ccacbd4f0a1f3f57b40f0/psycopg2_binary-2.9.13-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:81404c37e0344ebcf10aac127d33d35137e5dbab1daf9f3deee46188fd5879c2", size = 3052898, upload-time = "2026-09-09T23:55:32.961Z" }, + { url = "https://files.pythonhosted.org/packages/b3/5d/42a8935ab280e8dcd7c07a655c0c3d25d62e9e242be1961ac14630f1294a/psycopg2_binary-2.9.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:feb7b1856f6ca805cc0e08739858f6cdfed8ce903390126af30343c62899a389", size = 3355265, upload-time = "2026-09-09T23:55:35.071Z" }, + { url = "https://files.pythonhosted.org/packages/87/c2/0e0ffb4caeb651631cbc6c8ead83e2a16457750b1d2eb7f5ef111c1f4d36/psycopg2_binary-2.9.13-cp313-cp313-win_amd64.whl", hash = "sha256:691da68ae5dd7c3ac77514357d35ece7b1ba8b5f3e6c92735198aa6159c355c8", size = 2767914, upload-time = "2026-09-09T23:55:37.14Z" }, ] [[package]] @@ -3568,15 +3557,15 @@ wheels = [ [[package]] name = "pyright" -version = "1.1.411" +version = "1.1.414" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/1b/244c7b710031ada80f27e579ec20d28a2285dfc318fed0339866b1047f12/pyright-1.1.414.tar.gz", hash = "sha256:523c0a97c60da6333234955c277730c9cf4f5bd6d5399e7b7d2b0fc5d3599524", size = 4154638, upload-time = "2026-09-10T12:26:53.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ba/18b6e682ead424ad24bcc134339ae5d1b931cd9ae260540592a058a91279/pyright-1.1.414-py3-none-any.whl", hash = "sha256:2a6b4b3298c9eec174c5ed83bd338de6eee82df2992f3e1930e6199d381be36f", size = 6225049, upload-time = "2026-09-10T12:26:51.427Z" }, ] [[package]] @@ -3833,42 +3822,42 @@ wheels = [ [[package]] name = "regex" -version = "2026.9.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/c1/6b30b775c7bcc6cf6506a4d4741c2123e8d99cd50f3fe8cbd731f5fef526/regex-2026.9.3.tar.gz", hash = "sha256:aabd43208e335f4c3f0b56de3464b066dd425983a58f6eeb5738bcd7465403db", size = 416720, upload-time = "2026-09-01T00:53:43.821Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/cb/cba530bc3b068fc337f8f455c63ef5ee91a4eb4c76ecf5998e5cef5aaa6b/regex-2026.9.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5db80d0b1c8238940b5957dd66b5c818ea40a221f6652fb717c027a562d09c77", size = 496699, upload-time = "2026-09-01T00:50:27.98Z" }, - { url = "https://files.pythonhosted.org/packages/81/39/f2e9fb6bbbc80f8bf67ad79d7e2e8866f7837d7c24c692f7faf8f1272e7e/regex-2026.9.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:35d48ce3dee087b63b15cd0a7a3110d0a76c29edbe1f2ad0520b8c4adb7cb596", size = 297018, upload-time = "2026-09-01T00:50:29.487Z" }, - { url = "https://files.pythonhosted.org/packages/a1/b6/c16ee58840baf7659def27ef6f62f3d9a9909670d3c1b4b98bb8b8ee47e2/regex-2026.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f22e0d21ae7016c77175c139a7fca465b988efc1280df4816c79752068d9e2e", size = 292008, upload-time = "2026-09-01T00:50:30.929Z" }, - { url = "https://files.pythonhosted.org/packages/cb/b4/4987bf0f17604669b4ea5aef219886d0a73188c4716ff3a7d275d4d15c15/regex-2026.9.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:233662cf8cfdfe3c0e58aa8f7bbefc579b5be0ac34546f123c159804179e8687", size = 796101, upload-time = "2026-09-01T00:50:32.486Z" }, - { url = "https://files.pythonhosted.org/packages/0b/95/2a9ab02a68c8a61dc0b4882ed643b1a95740d9dc291dc26c77d19af79691/regex-2026.9.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2eed2e4d231278a2ccab3f4bfa2c1e39855f336475f7756a281d767d2b1753", size = 865435, upload-time = "2026-09-01T00:50:34.171Z" }, - { url = "https://files.pythonhosted.org/packages/04/92/0570d41559b446c97c1148cb9ebc1df09f2949b03c7c9bfee09976b3465f/regex-2026.9.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e674cecb61cb160be392da07fd8a71509ef927f437fbf3215432692ed385151", size = 911828, upload-time = "2026-09-01T00:50:35.72Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8b/9cc6d4123033f7cb82df6cd8ce19eb0fc18a964afe060a03c9b26757c9f3/regex-2026.9.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:665207e41bacd435db001099eeab44103197c2c1a729d73ade74688a905ed4ce", size = 801965, upload-time = "2026-09-01T00:50:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/c9/98/39262e91aa87a67c82cbe90a0df4c3d382c7a44811fe80067904085211b4/regex-2026.9.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a7ddc9a8ca1795166a1ca80364b8ce74187fc210e112d3fb048b711b934f36c", size = 776192, upload-time = "2026-09-01T00:50:39.57Z" }, - { url = "https://files.pythonhosted.org/packages/24/e9/3bb93fe4ee4b6f8ce7ba69b527c4a63cfa3393fc425ab26486041fe441c8/regex-2026.9.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3037d02425863ce9501afbaa04ba967162810004bacde39a53ea9a5b740eb32", size = 785053, upload-time = "2026-09-01T00:50:41.156Z" }, - { url = "https://files.pythonhosted.org/packages/f9/05/31d5bc2553a700c0dfc6b5b6a13c61cdcd1210fde1e304cfa18a33f138b2/regex-2026.9.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3de4eab8c763393b75bbb26f81934ab2cc8794f48f79e90622e3ab7ea57f3d14", size = 860546, upload-time = "2026-09-01T00:50:42.746Z" }, - { url = "https://files.pythonhosted.org/packages/65/a3/2e1e854d80becda0f061093805bbfc037a5849448f46d0a2b71a070d45e2/regex-2026.9.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:98620c9c4c22568ad70f57b80527c780b6f8fd26e36507bf8e2273262a228275", size = 765841, upload-time = "2026-09-01T00:50:44.5Z" }, - { url = "https://files.pythonhosted.org/packages/6a/d6/43d02948cedde2e8476ac893ea02755ee5ee1b21c531fda92d80e114f0bc/regex-2026.9.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0b1ba3aaaf5776de473ee16625ac60ac195abb0343afb273575a8201d99be089", size = 852147, upload-time = "2026-09-01T00:50:46.474Z" }, - { url = "https://files.pythonhosted.org/packages/21/ff/adb4e2d08afe8f4c6df004d94604257e1f72af7ba328af7715601585aba4/regex-2026.9.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56d8659c65166641d8f1b5efccc391c62c8a899eff4d528b981cc62b7b402a4b", size = 789761, upload-time = "2026-09-01T00:50:48.749Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e1/1490d1351758e87f6e702cf2025036bdc7bc59182e2ff5c7bec004b19aed/regex-2026.9.3-cp312-cp312-win32.whl", hash = "sha256:837c1859913798d8bebcd98d4a037e113f8d79e81733009bf590e449769eecb3", size = 267150, upload-time = "2026-09-01T00:50:50.414Z" }, - { url = "https://files.pythonhosted.org/packages/d5/49/4c40cf722d84d60e807a08ef4c3f579216bf97df60c4a1b10be49655d302/regex-2026.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:1ba1dbbb93c5c5629c1861763aec5bfa9f05ad24ef450694130e25029ce7bc36", size = 277773, upload-time = "2026-09-01T00:50:51.963Z" }, - { url = "https://files.pythonhosted.org/packages/aa/af/c48b3b2b4244b4b090554c78d3387e9ae7b859f3dbf7148a27d427e9e5b8/regex-2026.9.3-cp312-cp312-win_arm64.whl", hash = "sha256:d7b3a8a4bbd83ad8b29758f5d24bab10a3f2de87970db36f1e3651c733353136", size = 277122, upload-time = "2026-09-01T00:50:53.778Z" }, - { url = "https://files.pythonhosted.org/packages/9b/d8/1fb6053247efc5b5a1d7b3b7881dcf42861f8ba46bd72a2ff126469d4053/regex-2026.9.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d9148e47cfa1a067138867996b1d5d825de0132fed8dac3c92eebaaf312d280", size = 496442, upload-time = "2026-09-01T00:50:55.351Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b7/0b9c1c0385365ad12deba0bdf93a70ad9f97d1a919cc5699c33b449ad662/regex-2026.9.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:33c2860b73ea342c0a42bee9ebe3b3a0de3d68c580c4dcb52241cf4b6663731b", size = 296913, upload-time = "2026-09-01T00:50:57.251Z" }, - { url = "https://files.pythonhosted.org/packages/e6/eb/5750ebabdb010ffb0d31fceae68c7a8f3876c06140c3bd6a7feff6240d6d/regex-2026.9.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e33dfc13c02d9c4e55bcf3f3b2eb448537823a6f6f30bf737b2974b63a530bc9", size = 291783, upload-time = "2026-09-01T00:50:58.892Z" }, - { url = "https://files.pythonhosted.org/packages/96/12/3ee1542b6428a0955ac5a31562d87966a417c0e0a4c1e5c62ec822fe78de/regex-2026.9.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e84252a16234ee860206a738f9a5084f830a5d0a1370a418d3af4e917f5e08", size = 796114, upload-time = "2026-09-01T00:51:00.5Z" }, - { url = "https://files.pythonhosted.org/packages/0a/24/5ad415b80958d79f2da66a1997743ea8c38a1e9e2f63660f7cbf9303eda4/regex-2026.9.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3077ace9bf59f8513c8471a817a5af63699987dc024f535c1eac3447a4d70211", size = 865524, upload-time = "2026-09-01T00:51:02.25Z" }, - { url = "https://files.pythonhosted.org/packages/2b/8a/c298d469f5dd12b11d4e7b8c7714c808b9edace73118eaf753fc327c9429/regex-2026.9.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:635482cd183a1856da75a39c473a2e222697b7927f1955f586db83fc8a5da17c", size = 911948, upload-time = "2026-09-01T00:51:04.099Z" }, - { url = "https://files.pythonhosted.org/packages/3f/a6/e9a59b507cdf7a9735df4bab92b4ea9d2ca2e665c6de14c112db7e3c0926/regex-2026.9.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27f0809798071f56fb1bc536bb93714a95e8ed2ec0dfd869f095deebb30fd11a", size = 801945, upload-time = "2026-09-01T00:51:05.878Z" }, - { url = "https://files.pythonhosted.org/packages/56/1d/443e5541fd23d97c841ccddf90dbc9a964bd00e0ec9219b45a3eb0cde552/regex-2026.9.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d514026ca1c473cc14440e4d7bdf6721c642455b647a74c8143c0f22da358c28", size = 776223, upload-time = "2026-09-01T00:51:07.58Z" }, - { url = "https://files.pythonhosted.org/packages/7e/ae/78d48b14582f5733facaa2e001499b98707d8b8b1b31ab7559053c18f168/regex-2026.9.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b9190d4901d7786af9ab0ec46172e27cf7d72cdba2b82ee38eb40aadd3239a6e", size = 785072, upload-time = "2026-09-01T00:51:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/55/42/2f4ac830de12d264c1486f749988003a7f5a0366d7557a75de2418461466/regex-2026.9.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6c0f60b05cc708e6cdf68dbca86b7a36d99695962db0189bb1b3884ca3b28e90", size = 860644, upload-time = "2026-09-01T00:51:11.558Z" }, - { url = "https://files.pythonhosted.org/packages/47/50/6b44c6cf56f353744e6066a1310177b7b195afb11d33dff4d6cf3ec52720/regex-2026.9.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b7b7e6be82fd6d5256adabb82253c5c307de981cc20c0ce4cff0cbe6de88529b", size = 765862, upload-time = "2026-09-01T00:51:13.371Z" }, - { url = "https://files.pythonhosted.org/packages/64/0f/c5a8023dfc988cddd2c614b1afbe547aedab1ffebd5b5d9cd58f6ab28908/regex-2026.9.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b5f85bffdfe17da7dfff78eb32b261c27f3cd64c060033645079493f4cebc8e9", size = 852081, upload-time = "2026-09-01T00:51:15.324Z" }, - { url = "https://files.pythonhosted.org/packages/8f/f4/ab0ba467ebcaecfddf72ea7b3527a6088863094377c1303a3379284ef621/regex-2026.9.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d10a442c6450ebd35aa89392b8e4b0459ba474df63fc5e6828573d0a31a627ec", size = 789774, upload-time = "2026-09-01T00:51:17.057Z" }, - { url = "https://files.pythonhosted.org/packages/4e/fa/8e2f3021ff5ee3aafc80eed8f16663be5e16c7b663d72e4bd2c9e71e1433/regex-2026.9.3-cp313-cp313-win32.whl", hash = "sha256:63fa79eab192623acb169de1dfe8e733598c4047d06f7712347d1bb810a5ad20", size = 267125, upload-time = "2026-09-01T00:51:18.837Z" }, - { url = "https://files.pythonhosted.org/packages/6d/25/6d20a309c2e4b554cc33579dd55b0bb50d0c2ace7ce6f084be2327c330fc/regex-2026.9.3-cp313-cp313-win_amd64.whl", hash = "sha256:185c1ae881856208dda05708b6c908aff76878e59c998c8548d365c1bbcaf1bd", size = 277741, upload-time = "2026-09-01T00:51:20.583Z" }, - { url = "https://files.pythonhosted.org/packages/cc/b3/0282ae58fc167953809a968850622f89955e0fd70a65df882e5d488138b0/regex-2026.9.3-cp313-cp313-win_arm64.whl", hash = "sha256:db6538d733047f9ce4b74ee29c77643a1f99e4ca36e273495da93fbeedd2f03f", size = 277123, upload-time = "2026-09-01T00:51:22.332Z" }, +version = "2026.9.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/5c/f403115361de25809e8f785686ec7096e30fef73be9ae35aa51da4e80abb/regex-2026.9.10.tar.gz", hash = "sha256:1e321e2c84f0e52c457f5ea5944f796d6e8e09cb99738ea98dcc1bfe402a128d", size = 417072, upload-time = "2026-09-09T21:00:21.521Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/c8/bfbe893e90ee0148bd2860dd086f09b5d2080ca2b125f740c2e118c16982/regex-2026.9.10-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880ac684c27176464c00c3fdc456116364f5ebc70da07aad0c2d4a7ba45e98db", size = 496609, upload-time = "2026-09-09T20:57:09.987Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ac/56d5ae6efb759255c3b3db650a4be25f96a844ea3613b91e1e189a3b7294/regex-2026.9.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9d36b03dc362aa40ffaaec9d9bd75e87763529563ec008c43b0e07782f5be7a", size = 297024, upload-time = "2026-09-09T20:57:11.35Z" }, + { url = "https://files.pythonhosted.org/packages/60/4b/0f2d5f6bbb791cc10f22f0ed16c487e630dde8fa8fa0bd92a2bfe21a4b20/regex-2026.9.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866de9f98df0611d7b62b3a8729d3284a64c0cc6edd90bb95a533e443a4939cb", size = 291905, upload-time = "2026-09-09T20:57:13.127Z" }, + { url = "https://files.pythonhosted.org/packages/89/51/3fb5fe0d32f4cf0bc982286722c729a8d6f522d2fa2d5d14a702d9fc87f8/regex-2026.9.10-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c66d54042a14a503907d81861b8a5235e6d1f03d4fbc1d8767f652eaf957ac1", size = 800055, upload-time = "2026-09-09T20:57:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/89/46/ee507bd2f9d4420f26a594b35c551d7194b66f5d7897f63730fae6ec05c1/regex-2026.9.10-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:032da15431c890d376f53547f0a6219f4f4cd19f3e4f11bdc321453b5bd207e4", size = 871133, upload-time = "2026-09-09T20:57:16.674Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/cbaa90689684f91b1bc017e7f8c6d9425c6bd299108db02482dc51376d8a/regex-2026.9.10-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:23ac9a28180f274d7dd7651fa131ad5b02d343b75df4b040737f0356223895dd", size = 919627, upload-time = "2026-09-09T20:57:18.402Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f5/dcdf5e0d898024005cfcce631e3e934d111dfbe177ca0b7f253ae8a735a2/regex-2026.9.10-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e67f8843f0e4b931f1fa860bf3bbe4134b714c0155cc5c7c0d7ea450230aae0", size = 804587, upload-time = "2026-09-09T20:57:19.859Z" }, + { url = "https://files.pythonhosted.org/packages/73/70/eedfe81c29bae266a06ab4250978361a9bccd474704d88d4f4ef4506dff8/regex-2026.9.10-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bb7774924f8cd69f49cba0b3c2d679a6326f777e0e67d130ad5203e4df53f0d3", size = 777320, upload-time = "2026-09-09T20:57:21.62Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e5/86b207077efbcd91305700488f170b7eb1e1c54721cea74175273aa3b9a4/regex-2026.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c32480f3371b75068decaf9e5da72c224e953830dd71e36e06cf80e30ea39d8", size = 790572, upload-time = "2026-09-09T20:57:23.048Z" }, + { url = "https://files.pythonhosted.org/packages/b2/92/f622c3b2323f4c035b98e80221740a442127ad7993135b814f52057430db/regex-2026.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d2d377fd1cad611b806cdd732d86b65f536c768209890cb442556548daa65a23", size = 865485, upload-time = "2026-09-09T20:57:24.658Z" }, + { url = "https://files.pythonhosted.org/packages/4a/be/34bd621d3d6ac906ad67e57ed56c40cd45f7d51b9c0328335e97a7cb8ecb/regex-2026.9.10-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c014641157e9049b0603b8daa5343bd408d9b757b709aaa0f373cd3fab2d7944", size = 767925, upload-time = "2026-09-09T20:57:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/0d/28/ddbf7cba86f2adf5038c6c16aa829636ffc6e437f81bb0cbf302899cea5e/regex-2026.9.10-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1562aabd9d4eb09bd88a62ad97ed06800094b529ac43419e43020b9cefec79b0", size = 858800, upload-time = "2026-09-09T20:57:27.901Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f1/e8d7656ff3d3bd32e881d32f540b4981c79dee61908d6b790a45966e6895/regex-2026.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2479171edccced52ef02b899558f88ab2c235fe05b93180fdcae1670aacd89e1", size = 791648, upload-time = "2026-09-09T20:57:29.786Z" }, + { url = "https://files.pythonhosted.org/packages/fc/65/eac1a79115c8475d8ff539602eb54031564d719fdea5a599c4b0a1a26d1b/regex-2026.9.10-cp312-cp312-win32.whl", hash = "sha256:239620b0e0681669367c0e218c8eb2551d9f8fe3b9fccfc8d0003377804e8348", size = 267326, upload-time = "2026-09-09T20:57:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d4/4dcd0a05d3e97ca829165df1c39717f899d1d484dac8a6032439f2cb8d6d/regex-2026.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:4db7d00c4afbfbb55b8e17b1e371da11418ea9389b030acec63c1fa4c7ad4b86", size = 277933, upload-time = "2026-09-09T20:57:33.648Z" }, + { url = "https://files.pythonhosted.org/packages/55/f8/22617a80dee28f2451011eae36bc26b3d78c4994ba87b5281d60acf9b6c0/regex-2026.9.10-cp312-cp312-win_arm64.whl", hash = "sha256:c25a754bb81a2edcfc3b65eda50f017d736f818112ed43e8aafd595cb00678ae", size = 277447, upload-time = "2026-09-09T20:57:35.156Z" }, + { url = "https://files.pythonhosted.org/packages/20/90/d4452bf1ef7dbe406980e8b921a257024482203c1dafac535eae207611bc/regex-2026.9.10-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ef5a059ea1c6ee5d1c7e99a2484e628608d010921efe876c6f0e2029d2f35eca", size = 496408, upload-time = "2026-09-09T20:57:36.757Z" }, + { url = "https://files.pythonhosted.org/packages/6a/35/c763c6424a0f99d021d46dc1f9065147bb5a40c2b2cdf28d2ebdbcd96508/regex-2026.9.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dce932f8e3ba936475ea3d0d8b59f7b050a9e206e994f53f8fd80299871e87da", size = 296931, upload-time = "2026-09-09T20:57:38.811Z" }, + { url = "https://files.pythonhosted.org/packages/fa/68/241f88458b17c46ed2f80147a60a03b2ada7fb815c23b6bc76c298abb0a5/regex-2026.9.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d8c668af8f7bdb1d18739c27d30cd9f4b371495a883f75a002fb7a39d740fecd", size = 291741, upload-time = "2026-09-09T20:57:40.482Z" }, + { url = "https://files.pythonhosted.org/packages/90/9e/974d6de404c63e2d09525f4ddb99874c7ab8e1f781ccbe0dd3e26fa6f6e5/regex-2026.9.10-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aebdd9a946de328b3f6f61dbf48dd064a36eb6dddf96e34ae6651d37f6e9383", size = 800088, upload-time = "2026-09-09T20:57:42.098Z" }, + { url = "https://files.pythonhosted.org/packages/9e/fd/3875b73f9e7ba3321dcaa02c19f650c05c61345328acf84599ac6f45ceed/regex-2026.9.10-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f2374c27deb189b282ec7e16106752c22ad39b056bbd8018960b1e4cc95d67a1", size = 871212, upload-time = "2026-09-09T20:57:44.03Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/2358e791c0e171194dd6a8b97b520579098a21397fb79dbe6b7edc9e3fa7/regex-2026.9.10-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e0dc78251154b66dc60211563fc115345da332eaa881e4e2523fb1edae3772f4", size = 919752, upload-time = "2026-09-09T20:57:45.691Z" }, + { url = "https://files.pythonhosted.org/packages/20/3b/000c79c3f9c06b7542225a5d3a7f9a85405da7224b3b9af94a491d07abea/regex-2026.9.10-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bafa41b0dd63669e5c0f8adf3d24819efeb73c847f492eb011212eb352e69041", size = 804578, upload-time = "2026-09-09T20:57:47.548Z" }, + { url = "https://files.pythonhosted.org/packages/30/6d/195eedb1de87f26639191e7487e41eb81e2ce255bc7563a64f3f5a95eb08/regex-2026.9.10-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ebb2ba68e4641a994061f70bf44ed448fba0b9b1d18c94ffb9efc1cca805b39b", size = 777345, upload-time = "2026-09-09T20:57:49.63Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/11fe2b313fcd92cb75c583648f2746031b9f4da9e9ed4241204a5e8b3721/regex-2026.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:048a89ee797db10160bd2bd519286577a6b43a100279bd4b7d8456a3d69c80a0", size = 790556, upload-time = "2026-09-09T20:57:51.27Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c0/07ec9b4c43b0e16d62454971a5ab3886eccb0bfa161300a02d801ab28620/regex-2026.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:79e9432995e14c749d34209413de5e621ec8e67789bf4f46dbfabea9d06a2406", size = 865572, upload-time = "2026-09-09T20:57:53.163Z" }, + { url = "https://files.pythonhosted.org/packages/19/07/43bc9a9cf9fc8e37d2ba47980dfe4a6e151d2cf3ab969e0031e2a9b21484/regex-2026.9.10-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5847e22bbf959764d776937d791d034cc2d19b787e361c88d97e859e8dc68502", size = 767971, upload-time = "2026-09-09T20:57:54.805Z" }, + { url = "https://files.pythonhosted.org/packages/9c/49/3b9286a3a94f3c89ed4ddbe74e72bdde21c1a5eadd520d5f4ed4a61936cb/regex-2026.9.10-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c103b3b14e011774af4fb7e4617ad4d72b9171905cd3b231a70a4efd76e477d7", size = 858835, upload-time = "2026-09-09T20:57:56.627Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9e/e5d27ce9fee8e3ef95f886c7b6ecec211efa4cfc18bd73bd5cf26cca4741/regex-2026.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6b34a778c695d24e77c140e3b4c95da69282e34f2f6b02b55656aa4a0379f643", size = 791793, upload-time = "2026-09-09T20:57:58.313Z" }, + { url = "https://files.pythonhosted.org/packages/63/03/c28a6bebedc3e2d86ee27ec2de16f7ec0419dcd10e771d43dcc9c58a2e99/regex-2026.9.10-cp313-cp313-win32.whl", hash = "sha256:7abb38b8c40f3a235235a44da452c64b7b5c1d650ec6351027db0e090804f2e5", size = 267298, upload-time = "2026-09-09T20:58:00.009Z" }, + { url = "https://files.pythonhosted.org/packages/cd/fd/5c85fa6cfb8e034080bda5a72fa0a4df2b7777a35eb7e73c2799c2adda7a/regex-2026.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:20e8bfb07ad79a282f8b95b56fe67f9750b1b7f775724e4ba1f23cb296115ce4", size = 277894, upload-time = "2026-09-09T20:58:01.731Z" }, + { url = "https://files.pythonhosted.org/packages/c1/28/f5a25f6f65501675977fda35d9f61abb1468c4b87c0f73e536d8b21a60b8/regex-2026.9.10-cp313-cp313-win_arm64.whl", hash = "sha256:3bdeed3318a8eb2bbadc9c56347e0ff651639e934a47e168d05a3b12929fd0e7", size = 277436, upload-time = "2026-09-09T20:58:03.422Z" }, ] [[package]] @@ -3973,27 +3962,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a4/7c/6adb35d70e7c027e308274557901c7e00fb3407750faf3620c184ae058cb/ruff-0.16.6.tar.gz", hash = "sha256:dcf8a73d2ff77e99dde91244b4da16feba7f14e6beeb4015dee7c5a909e99050", size = 4921251, upload-time = "2026-09-03T16:57:29.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/28/9cc1b79639e284ec103f43c88c644db4eb58cbd0ea1ca11f1193435369ac/ruff-0.16.6-py3-none-linux_armv6l.whl", hash = "sha256:61c368c26bf8e973e5ab14a2772de587bc068ea3f9a277f673380749b4898fb8", size = 10015638, upload-time = "2026-09-03T16:56:40.986Z" }, - { url = "https://files.pythonhosted.org/packages/71/11/627d342ef727ea7794edf74fe23d60a074b02c3acc2e9436684e782286ca/ruff-0.16.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ecf4f068e2e123e43a26e9db4e19524cc56563912404e83bbfca375757e45a32", size = 10220762, upload-time = "2026-09-03T16:56:44.681Z" }, - { url = "https://files.pythonhosted.org/packages/43/d9/b75668ce41e4c8d073d18d6d08672ba6906ce45d5c06ea4fdb2e84ce3853/ruff-0.16.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:99b62ea33baf130f50368798d841f0d95527b6d817bf31817b65dd058f1d314c", size = 9835082, upload-time = "2026-09-03T16:56:47.142Z" }, - { url = "https://files.pythonhosted.org/packages/99/97/123ab10b05cde889c107c20f5a9774955104b5552796a2a8584b089ae8eb/ruff-0.16.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fbf89013f2bb3f6835a6038ff658dc8a1b38c98dc8e724b964168ad4e881876", size = 9949304, upload-time = "2026-09-03T16:56:49.813Z" }, - { url = "https://files.pythonhosted.org/packages/3e/58/a4a2c59dd2e5b85929c912d9cac3056eb9ee8c7e75e9b9fe3e109174966b/ruff-0.16.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56a67065e22efa6bc4d498299d3bb06c0c90aace8fac2068b5a12f9dc4d8d51d", size = 9840612, upload-time = "2026-09-03T16:56:52.368Z" }, - { url = "https://files.pythonhosted.org/packages/61/6a/ff8c8626a786c4f49d48ced4a752dadbca65f5263005f9c2416578194694/ruff-0.16.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e25cc89174874b176a157e4428d66761c2c0c006654419bf384f967f361ff1b1", size = 10543465, upload-time = "2026-09-03T16:56:55.089Z" }, - { url = "https://files.pythonhosted.org/packages/ad/bb/c47535923365f337b82e28192e4e9eef2176511007cfd99a62fc22df5dad/ruff-0.16.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700580ed5303723cb3c11c2f1d2a8913ce77b7ea86646dddb887f5417a9ba70", size = 11267576, upload-time = "2026-09-03T16:56:57.791Z" }, - { url = "https://files.pythonhosted.org/packages/ba/50/e5119a5212b5cd63b51e1f4b25e7bd636a6668fc069a3160b108ad7e3c16/ruff-0.16.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15f1d0b6e165a6e56567befb6629f8209271311d990bae0f37e6d065035ef5f3", size = 10781993, upload-time = "2026-09-03T16:57:00.666Z" }, - { url = "https://files.pythonhosted.org/packages/8b/98/083d8b4ef3c51a0d19db84367791cbe9f44e4b53343d19dfa83556e1cd9a/ruff-0.16.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d72c591a96986ee4268860e2b7235082129ca5e4cb9cbba653a4b57c11893757", size = 10317748, upload-time = "2026-09-03T16:57:03.428Z" }, - { url = "https://files.pythonhosted.org/packages/9a/29/68f7ff2c5ad95f19f00627ac2de95644e25fe47371ea60b2db1fd952315e/ruff-0.16.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:65a006baa18f33324325814c864daef03541d51564b98c517610ea756ab7003e", size = 10540096, upload-time = "2026-09-03T16:57:06.182Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f9/79a8f6de85968641d68a7863aeec577551924ef066a990a48ff93167beab/ruff-0.16.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cd02a7bf1a21a8735228a3e8c95a9dc5cf86bd2a52194f4aaae2a5755b4de0f4", size = 10100494, upload-time = "2026-09-03T16:57:09.194Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e8/b81a22d9b90c00b892ccf2fa2ac36fa95de4c13ab85aea3e73795cfe4651/ruff-0.16.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:31b36f1e5ad85e0737f09d2be4e512e2e283583c14015da3b9dc07359ac0fc88", size = 9843663, upload-time = "2026-09-03T16:57:12.168Z" }, - { url = "https://files.pythonhosted.org/packages/39/aa/54f516ec5e5a11c4afdceb1c454ebb054ffb96e4f4a1705580b4346abd35/ruff-0.16.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:61029b4ab4aa723fd3064fab96b1d814492596bf0c792679fffcbde1e1679953", size = 10282461, upload-time = "2026-09-03T16:57:15.077Z" }, - { url = "https://files.pythonhosted.org/packages/52/0b/38d0aa8aa32372b96dc44f97b22e576c4147808271aab7b2cb1e353d4445/ruff-0.16.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9ac8998457832c2061709d900856b7ad271dace0cb41f346588d540162bfa718", size = 10728808, upload-time = "2026-09-03T16:57:17.797Z" }, - { url = "https://files.pythonhosted.org/packages/5e/e5/9e274e24eeb027640ffc7442f21239f16d17f47acec15ae34f32e03a5c79/ruff-0.16.6-py3-none-win32.whl", hash = "sha256:0b87d9d16fcb63e8018423ca1d50b7260f15cb2da33e30db4baad4183a948c25", size = 10049212, upload-time = "2026-09-03T16:57:20.55Z" }, - { url = "https://files.pythonhosted.org/packages/22/31/72472449414223ed1a2da236b992adbb1a2ae59e34794574810f60ce068e/ruff-0.16.6-py3-none-win_amd64.whl", hash = "sha256:10d21c51c3495d8eaea7b703a16592117ea6eb1d649e36335aa965ff1173eb39", size = 10556402, upload-time = "2026-09-03T16:57:23.501Z" }, - { url = "https://files.pythonhosted.org/packages/fc/07/d781f8f8e1ac24bef9f3269cf62ffb1407ca24c3a8f12e5e22874f90528c/ruff-0.16.6-py3-none-win_arm64.whl", hash = "sha256:7a976c79b958f94e50a022a19f0f8c87387448020935ec14fc74331bd0a7f2c5", size = 10412850, upload-time = "2026-09-03T16:57:26.416Z" }, +version = "0.16.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/bb/5a449b9162e49b139d72f61672bd3ac1d790221f796d3304e2241fff4c58/ruff-0.16.7.tar.gz", hash = "sha256:5f71d004ac1263b22fa39462ac5ae618a4b77d58981af2cc79bf79a29c12b1a6", size = 4924184, upload-time = "2026-09-10T18:04:06.336Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/b2/c80aeeb7f9e469c0d63a85d2f1ab6e1ebfbe10ea7a8d2438b7e09e3ff09e/ruff-0.16.7-py3-none-linux_armv6l.whl", hash = "sha256:727307773e7c7f9181d3ed3a2484186e56c1fa1874255911c74585eb2c7c19f9", size = 10048917, upload-time = "2026-09-10T18:03:30.28Z" }, + { url = "https://files.pythonhosted.org/packages/7b/96/20bb7bcae008004df52afcb7ac83432d4a467f2c17b672fe46d26be231c5/ruff-0.16.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9d61c258deabf58f34c67bd4bb4d939c7f2e6b5f0e59c1cdd1cf771b11cde929", size = 10242929, upload-time = "2026-09-10T18:03:32.706Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f184b0d5abec02db69cfd7e49b688ae0237554528ca777136c613bf36bee/ruff-0.16.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ab81118df8945e0193d0240712aa4496573595b75185c3636ed825592a0f728", size = 9847245, upload-time = "2026-09-10T18:03:34.509Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2d/db1633a641866ed801e34cc6b60ef236c5e16f9b2124ab1d49cc24a5fe4f/ruff-0.16.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c196c968874fc8019da8e7163de7a1a370f111e2309b4b7dfea0fce950198d0", size = 9961780, upload-time = "2026-09-10T18:03:36.618Z" }, + { url = "https://files.pythonhosted.org/packages/4d/98/edea21e1a3e38dbbc3bf6bb068b863b3b06184cf8533a4c7dbbe208a89d5/ruff-0.16.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac8c3bd0a7e10ad31e6ce51e7a99f3cb772e69aecdd6b9ea7e99b362f62a62c0", size = 9866337, upload-time = "2026-09-10T18:03:38.805Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/a15e60d4c87b214646f116ca9d204475bf993ee1047459bc9a360fd4d6d1/ruff-0.16.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398d3988edde000b5c75dc1b3f584708da9bc990de069c18909142580fec1af9", size = 10562512, upload-time = "2026-09-10T18:03:40.71Z" }, + { url = "https://files.pythonhosted.org/packages/29/42/eaff4c9b6d0c7cdf56df313a17e89ae854f5bbc0b0c8f9cce19be0ab7a8f/ruff-0.16.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce05b62b770a8217c4646a9c4139fca00efe8fe5d71f87df2b243ff20d4584d1", size = 11302938, upload-time = "2026-09-10T18:03:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/c75aa59a4ec181fe2ec06cab30e198c1c6d107229a9f008ae3a7c16cabd8/ruff-0.16.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af1b576fddb9d9ef2ececfb5fadcd6a624b25070ed85e3cfcfe449fc3ff6a7b9", size = 10840857, upload-time = "2026-09-10T18:03:44.604Z" }, + { url = "https://files.pythonhosted.org/packages/21/33/81f3da371942ea031105ba679d8d6e28ec1660ccd690a45f42d381161356/ruff-0.16.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce7f8f22df67c93ed96c717f9128eadb797144ac2bad475cf536f31d6100c55", size = 10370001, upload-time = "2026-09-10T18:03:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/fa/0b/6345fb4dbf6dd0ed1cfe5d18391dc9c3f59cc81622a7b0a65b84b3e730ba/ruff-0.16.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:06d0e93d04f392996435ebd600c153f65b47d73fbec2415aa99c5ee5756b3a5f", size = 10548735, upload-time = "2026-09-10T18:03:48.658Z" }, + { url = "https://files.pythonhosted.org/packages/3f/4d/c5576adf511f92a328e5569dda190ecdd430da51f1a649f3a4a2fd73e21e/ruff-0.16.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:142151a5e7b93c1b11111337142f89dd2fbfee92161225c99a97222f22e32656", size = 10108496, upload-time = "2026-09-10T18:03:50.563Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8c/667d83c16199a17a56adc6b0bd4c3beb5b767a2babcd16a56f76f9be7fd6/ruff-0.16.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e6651f97a342d8b35d54d8991544ca22169b86dc54111cb604666940c431b750", size = 9860136, upload-time = "2026-09-10T18:03:52.621Z" }, + { url = "https://files.pythonhosted.org/packages/99/75/78d401106731999a1dd20cc5a6961e37e1eb9397a3b589f73f3a5ce146a3/ruff-0.16.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ef140c6eb935fa9a84c9c607dfb2cb1b85843c192e79265b0c54f35f557ea8e5", size = 10286290, upload-time = "2026-09-10T18:03:55.207Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/56f9c3a8b755df93a0ad318b2147bf4ef5dae9a7e5ec61c460109c67957f/ruff-0.16.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:53e39506a730fadeee0d998ed5946f30671f0db240c6c7c73bdabbe33604bb6f", size = 10745048, upload-time = "2026-09-10T18:03:57.299Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ea/7f9b938a63ece4bec677ad7f9f7fa02df3383db1949ed93a382441c09a87/ruff-0.16.7-py3-none-win32.whl", hash = "sha256:2ea3470fcebcbc5df2fb0c6f3b90333fa9084c534e0111c038fa4a6ab9f1c4b7", size = 10059082, upload-time = "2026-09-10T18:03:59.632Z" }, + { url = "https://files.pythonhosted.org/packages/39/11/480a6973a927aa653e1cead6a6416008640e03a99d05b34c0434b8c6c366/ruff-0.16.7-py3-none-win_amd64.whl", hash = "sha256:7ac26aca826e9e21d0f1cb25b54ac660760a9fdd094d3e4df9848232be98cfc6", size = 10593368, upload-time = "2026-09-10T18:04:01.999Z" }, + { url = "https://files.pythonhosted.org/packages/8b/4b/51327018d056f0dad2c2238f26d1fb0f53707a9d91b75dea6d1b3039f136/ruff-0.16.7-py3-none-win_arm64.whl", hash = "sha256:aab7f39e2c9df6c596216070f98eef1207b94f8516cca20c808826974971855b", size = 10412401, upload-time = "2026-09-10T18:04:04.098Z" }, ] [[package]] @@ -4034,7 +4023,7 @@ wheels = [ [[package]] name = "scikit-learn" -version = "1.9.0" +version = "1.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "joblib" }, @@ -4043,20 +4032,20 @@ dependencies = [ { name = "scipy" }, { name = "threadpoolctl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/eb/eaf5e07fcc0da7149b0e084f24e54edd7441b9a89ce7e034032ae97fe3a0/scikit_learn-1.9.1.tar.gz", hash = "sha256:629cada3e33e2b9bf376cdc7614a47a4140b8aedc1d836579e359736fbd82977", size = 7786908, upload-time = "2026-09-10T18:34:04.679Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/20/75f915ff375d6249e6550ac740fdbbd66159a068fd3af1400ff62036b07a/scikit_learn-1.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2bd41b0d201bc81575531b96b713d3eb5e5f50fb0b82101ff0f92294fdc236ac", size = 8741122, upload-time = "2026-06-02T11:53:24.08Z" }, - { url = "https://files.pythonhosted.org/packages/cc/d5/2b5148f2279196775e1db2aeb85d14b70ac80e7e32b3b28e7ebeafb0901d/scikit_learn-1.9.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5be45aa4a42a68a533913a6ed736cf309de2226411c79ef8d609a5456f1939b1", size = 8261512, upload-time = "2026-06-02T11:53:27.183Z" }, - { url = "https://files.pythonhosted.org/packages/a0/ee/5adbc77656b71f9456a2f5a7a9fdb4bcf9207a6b962889f1c2f9323afa4e/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e50ed4da51974e86e940690e9a3d82e729b62b5a49f7c9bac534d515d39d86f", size = 8837603, upload-time = "2026-06-02T11:53:30.328Z" }, - { url = "https://files.pythonhosted.org/packages/6c/c2/63fdda36c56437eeb44aaf9493c8bcd62ce230ab1598924fc626ffbfa943/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:056c92bb67ad4c28463c2f2653d9701449201e7e7a9e94e321be0f71c4fef2b8", size = 9132097, upload-time = "2026-06-02T11:53:33.456Z" }, - { url = "https://files.pythonhosted.org/packages/83/a4/c8e67227c680e2259c8864ae72ff48b06e16a6f51253a22167aa02a8aa4e/scikit_learn-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:4306775fad04cc4b472a1b15af1ae9cede1540fbfcc17fbce3767cd8dc7ae283", size = 8211173, upload-time = "2026-06-02T11:53:36.602Z" }, - { url = "https://files.pythonhosted.org/packages/cf/fd/3c0863792e98e67e9184aa4029288a175935eb65443afcd30d4f143450cf/scikit_learn-1.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:26e22435f63bcdcf396b574273f29f13dd531f5ea035801f5be10ba1540a4e60", size = 7867451, upload-time = "2026-06-02T11:53:39.075Z" }, - { url = "https://files.pythonhosted.org/packages/3c/01/cf3310626b6d48d3e9be69a1223f9180360b5e6edb045f50fade723ce494/scikit_learn-1.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:80746d63bd4b6eaca54d36fe5feaf4d28bb38dc6f9470f81c7cad7c40155f119", size = 8705188, upload-time = "2026-06-02T11:53:41.964Z" }, - { url = "https://files.pythonhosted.org/packages/3e/04/5acd7ae280c5f93b6ac5ef6cdec14eef4c8d1cd91d85b3292989c94d96b1/scikit_learn-1.9.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5b934c45c252844a91d69fda3a34cff5e7307e1db10d77cb10a3980312c74713", size = 8228299, upload-time = "2026-06-02T11:53:44.817Z" }, - { url = "https://files.pythonhosted.org/packages/0c/39/ffe829a5b8ecb40a518724a997794657fdc354ada5e8fe8e64d998c0bac9/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:38c3dcb9a1ffb85505ec53d54c7b4aea0cff70050425a7760c2af661ac85df05", size = 8789690, upload-time = "2026-06-02T11:53:47.461Z" }, - { url = "https://files.pythonhosted.org/packages/1f/88/8dab5de10c638c083772a6be83a3d8106ced492f74a928c8693638e5bb50/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da76d09304a4706db7cc1e3ebaa3b6b98a67365cc11d2996c4f1e58ba47df714", size = 9087723, upload-time = "2026-06-02T11:53:50.702Z" }, - { url = "https://files.pythonhosted.org/packages/20/3f/7917ca72464038f6240ec70c29f94862d08a34a74291ae4d4ec5eb8186a0/scikit_learn-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5808d98f15c6bf6d9d96d2348c1997392a5888ce7097e664105f930c4bca1277", size = 8184330, upload-time = "2026-06-02T11:53:53.396Z" }, - { url = "https://files.pythonhosted.org/packages/78/c7/15739eb2f61fda3c54639e9942414e5a19ad8a8d1f5a3266afad7cb7df80/scikit_learn-1.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:d77f54c017633791bc0225a43e2f8d03745fdcfe4880268fcc4df15f505dec2e", size = 7840653, upload-time = "2026-06-02T11:53:56.035Z" }, + { url = "https://files.pythonhosted.org/packages/df/a7/25f0a43d2fde306e8ef45f45121192f687b79beaf4bae8c21607c46c5e63/scikit_learn-1.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0c0f8b5d09b44101cea2767f300680bada1ea27f976fe4b48b83950a4f55a49a", size = 8775209, upload-time = "2026-09-10T18:32:42.804Z" }, + { url = "https://files.pythonhosted.org/packages/60/ea/57e57539ce175d774fc291ed091b0a6d756854b92cd92554c6bb4d0ae498/scikit_learn-1.9.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:8c14ce41d561f7749f990b41d6703fe02c4669fbc485e598e069e0a1967b488e", size = 8295541, upload-time = "2026-09-10T18:32:45.069Z" }, + { url = "https://files.pythonhosted.org/packages/78/2b/5721a174406bfba49bce20ae997b3b64cf355c3f623a2638284ab6a82156/scikit_learn-1.9.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4c20a6c017d820faa7ac8c783e3d0c6a9a2e297bf9f55332ca17cdf7fd4d04d", size = 8871592, upload-time = "2026-09-10T18:32:46.999Z" }, + { url = "https://files.pythonhosted.org/packages/8e/57/a50162f3d29feb979ab6347c6debda506dfb525bcff3c50dd17606651c7e/scikit_learn-1.9.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e5d7b18a5b9dca241a74695f3275fa4c895a9dadc72b3d8df5fa9d1083c9b83e", size = 9166043, upload-time = "2026-09-10T18:32:49.343Z" }, + { url = "https://files.pythonhosted.org/packages/72/8d/27c054166bac671770d1ea0ef7716134fe8119c0df224a58c89fd735a61c/scikit_learn-1.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:4b59abb30618121cc46b45972d6bf53a7128b4df4cd346c6ca6f4d5f9031e49c", size = 8262238, upload-time = "2026-09-10T18:32:51.679Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d6/493086006ea0c68ad62c40a8dece1961b61bf503f45400f133d47f56e5be/scikit_learn-1.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:d5945a2908be62350e2978344e62b56c1552c2ca4f844ebf6277c94944d647dd", size = 7902102, upload-time = "2026-09-10T18:32:53.67Z" }, + { url = "https://files.pythonhosted.org/packages/bb/8d/b60d5e7354ff0ff5cc9400e60273696589d87a30b8b2235886a76d80d062/scikit_learn-1.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c2b312fd8c02951a364fa120ea08c1cec10d863466bf1701b013152d7537835", size = 8739300, upload-time = "2026-09-10T18:32:55.483Z" }, + { url = "https://files.pythonhosted.org/packages/2f/81/3c6392c03665d2899457a76e535a9a6f597dddddf3220fd2e1d790da88c5/scikit_learn-1.9.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:61cd968ab831a76d0ecbaf0347ab2270268716da28f94fd022497e3d6f205f13", size = 8262364, upload-time = "2026-09-10T18:32:57.966Z" }, + { url = "https://files.pythonhosted.org/packages/0f/35/a15b8653499692879821301d48059376d6e68e8b65cd0f22d19b6ee83cd9/scikit_learn-1.9.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5990f9c69e431bfaddcde1a6d7c5355243e026bc9b9e560c13893b90dab53fb4", size = 8823698, upload-time = "2026-09-10T18:33:00.632Z" }, + { url = "https://files.pythonhosted.org/packages/23/e5/688703d357e5393f708d98eb189fd415ae69e39f6de03c6bd4005aef6118/scikit_learn-1.9.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55e79d6e9b0923f1a978179822bd43d7f5543f45e970a00fe861f43486380aba", size = 9121732, upload-time = "2026-09-10T18:33:02.825Z" }, + { url = "https://files.pythonhosted.org/packages/96/45/a10add34c08184d373be9384660c75758128ca881ed27b503b6f6a742478/scikit_learn-1.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:2070f271e5375dc42c6bb93b461ab1c0aa5841d4009267e0cfd95a39dca94a43", size = 8237244, upload-time = "2026-09-10T18:33:05.26Z" }, + { url = "https://files.pythonhosted.org/packages/9e/08/7a89bcdadd1fff0d464d01056417b646c9abcbc54f7297a0a1203bba5ebb/scikit_learn-1.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:613f0a783ca05aa844a4e1ac42d48425058f2c52be73f40f8cd98b7cd111acd6", size = 7875398, upload-time = "2026-09-10T18:33:07.506Z" }, ] [[package]] @@ -4463,7 +4452,7 @@ wheels = [ [[package]] name = "transformers" -version = "5.16.1" +version = "5.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, @@ -4476,9 +4465,9 @@ dependencies = [ { name = "tqdm" }, { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/2e/ba418680ab901dae269360bb8642485eae04f1af91ee2ebb8bd6f3607305/transformers-5.16.1.tar.gz", hash = "sha256:17b0eac726ddc55e84ac58946063e0c6d37fd000c456b581f050ea0f4e822869", size = 9650542, upload-time = "2026-08-26T14:48:58.789Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0e/9e/750649904a065007a838981785b2bd8d9ff26154c6c341ac67d0b7f82c68/transformers-5.17.0.tar.gz", hash = "sha256:a153be279169b55b92d8000bf4af294aed684503d091cca7804da2dd8a9de000", size = 9817878, upload-time = "2026-09-09T15:39:56.886Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/4d/ee3728674c0bbc637bb4af88ccf0be697f92e4e90b55f5dc110c44d61b61/transformers-5.16.1-py3-none-any.whl", hash = "sha256:2f2d5b98a5ad3718713653734298fa620754ed683702a635ebb587df3ed29c7e", size = 12080592, upload-time = "2026-08-26T14:48:55.083Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d0/c502b60d684adbd98a8dc7d5bb866842772b816ac4354e4608be240041ae/transformers-5.17.0-py3-none-any.whl", hash = "sha256:78ec1ce21579b38dfb83950a0658cd119f87212a2fcfdff478096ce9d6c03801", size = 12295140, upload-time = "2026-09-09T15:39:53.746Z" }, ] [[package]] @@ -4507,7 +4496,7 @@ wheels = [ [[package]] name = "trl" -version = "1.12.0" +version = "1.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "accelerate" }, @@ -4516,9 +4505,9 @@ dependencies = [ { name = "packaging" }, { name = "transformers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c1/e8/2e131fd15224433c48b0c6afd80a2926a815e070dd2d9b528cc07c337056/trl-1.12.0.tar.gz", hash = "sha256:494ec6cfd07097bb8116cc7bec299b067dd765d1c19ed3eca2d874a29979f229", size = 818314, upload-time = "2026-08-26T19:46:16.456Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/ec/2ea0856273ec5a5414555c81a2b9a9cdc9dc91e518ce2da656912ccb01f2/trl-1.13.0.tar.gz", hash = "sha256:0905a7813e246c8449fc8021492134cdf80402ec9ad9ae7f8a72876f77eea9d7", size = 825648, upload-time = "2026-09-10T00:40:37.977Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/21/b29125ab2c1b324de93f863b9ab58237ccf87990dbd7ef7ef066bb1284aa/trl-1.12.0-py3-none-any.whl", hash = "sha256:9c8f117b21a941edbc85744f5a341c9453d38e81f24ae81d3c6e9b9ca3b96409", size = 992576, upload-time = "2026-08-26T19:46:14.821Z" }, + { url = "https://files.pythonhosted.org/packages/d4/66/94f4f8f2967a5ec02fdfa61c08423ffeba652d9407fb6e4f5e8eaf1c8e34/trl-1.13.0-py3-none-any.whl", hash = "sha256:7c7ae0c72e969450221cf4c7cfe69c532c5a83c727204f1c8ced38a9b79134ec", size = 1008465, upload-time = "2026-09-10T00:40:36.301Z" }, ] [[package]] @@ -4761,33 +4750,33 @@ wheels = [ [[package]] name = "wrapt" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ba/8dc25478ed234dacc7d83c671634f347d0bdfb65bf0502f41879cf2f15a9/wrapt-2.4.0.tar.gz", hash = "sha256:7082fc1f94b020ac275870c4af71b09cff22876fe6e9c4c0ad01ea21d217b288", size = 161179, upload-time = "2026-08-30T04:41:51.424Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/22/581a0b44349d5babe526c958f365b8126e0fbd8fc2810e80446c47358050/wrapt-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef4e2d6e399ce6eecc80179a6b9ef6544f121288f95fc132bc36c9d9503903af", size = 96374, upload-time = "2026-08-30T04:39:42.335Z" }, - { url = "https://files.pythonhosted.org/packages/5d/90/095984648cec62a786bb27c0b50f6cfa5856d1e073ba1006fe148d190084/wrapt-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b9b32d5e4f0a179cef5075cc79b79d6d3482c44c434c12969e48c6719e06d95", size = 96178, upload-time = "2026-08-30T04:39:43.789Z" }, - { url = "https://files.pythonhosted.org/packages/f8/fd/b20e3cb3cab35131b515edf18e8cd777dff680fc76fc00919481f4e536af/wrapt-2.4.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d7dbbdbfdacb85c2d962fa52db791c77943fd777d600d74c95af2d53b32f5a94", size = 227806, upload-time = "2026-08-30T04:39:45.264Z" }, - { url = "https://files.pythonhosted.org/packages/08/75/c8dfba5e0caf17cd0718a0cbbe76cb85e637a2d65183fb728232419f6fca/wrapt-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39cd68df4dff79f5336f9c745c06259d204bcb42d504040c9c91eac9e2abb39c", size = 229004, upload-time = "2026-08-30T04:39:47.068Z" }, - { url = "https://files.pythonhosted.org/packages/42/05/d4853fbd33e5860b10d5aec690f563547a92a82e61fb8bb2d4ece1ce3570/wrapt-2.4.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2a9f1a2f75bb95257cc5744e255e10a5a86e923f328b40ad3dbf9d8d03430013", size = 208934, upload-time = "2026-08-30T04:39:48.73Z" }, - { url = "https://files.pythonhosted.org/packages/a3/66/23d0e8de9b411fd198af5121627587563657370c8d509fbe5ea8adb3df79/wrapt-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8763ad01e3725b7751a4575f38bbcc19c0aa0822fec91c5c5bd21ce3ce7e1d2b", size = 225709, upload-time = "2026-08-30T04:39:50.287Z" }, - { url = "https://files.pythonhosted.org/packages/01/37/3b357bc90530d510ae59ae7ac48265c482ae899e47637ca4436645688b40/wrapt-2.4.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9125c6dbe8b88c00dd8ef4fc1e55757e8eb4720b6b2b2cc610a45bd32bd28c57", size = 207090, upload-time = "2026-08-30T04:39:51.78Z" }, - { url = "https://files.pythonhosted.org/packages/6d/0c/d8a5c6dbcc2d221308223bcea4130c6332454a855cb4dbd5dcb2360b13b2/wrapt-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:28f5de1526831b8f173889a436e289fe181ede8c66c9feb669d1aca8fd602eaf", size = 216269, upload-time = "2026-08-30T04:39:53.641Z" }, - { url = "https://files.pythonhosted.org/packages/92/93/cc9fc8fef1d3d25edaa1c2dc2337b556dc1d0613ddc1c4a6fe9ee08ad705/wrapt-2.4.0-cp312-cp312-win32.whl", hash = "sha256:a9ca1cdb3f7facb4990c7739ea5afbaceeb6728d066feedde03a4cfe83b29b03", size = 91187, upload-time = "2026-08-30T04:39:55.38Z" }, - { url = "https://files.pythonhosted.org/packages/ad/ec/a7b10705172bdb669b9687a8ff68bbe5f566437d2a49ad6d976af48b6d10/wrapt-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:8b464316489fb2fca0669ea0f8f07290054a0f26fc72982d3e4cf95469628ba9", size = 96423, upload-time = "2026-08-30T04:39:56.81Z" }, - { url = "https://files.pythonhosted.org/packages/83/7a/e838ac6463a1a1a1817b2f184ee2aa20c54692b80368c5063403c8d2461c/wrapt-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:db1285071ea09a7767fac608e7b5c7b03c09833b06186875a359905fbc659d29", size = 93003, upload-time = "2026-08-30T04:39:58.237Z" }, - { url = "https://files.pythonhosted.org/packages/19/86/f9de4e11582ff96ad2199eeeceaa17faa27bbdc599243f520070c4f3de07/wrapt-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5c5c4c728cd22a36e4b8bb5df4a7d3bccaa865d27725b36eeb3b6f18fb2e1bc2", size = 96041, upload-time = "2026-08-30T04:39:59.575Z" }, - { url = "https://files.pythonhosted.org/packages/c3/ab/1dbf50802bea3b46192fd0dc39bb0eb2e77a064c813b2bbd88d2888ad49f/wrapt-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7de5b8d94417e55c02be50cc226e0ae1209bbc73813bf691dff3979c94438115", size = 96269, upload-time = "2026-08-30T04:40:01.182Z" }, - { url = "https://files.pythonhosted.org/packages/cb/a3/a3b5cde1cd06e04b6e95134eb3187a0a7da607a530e7795b221d4e4fa819/wrapt-2.4.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6436e2bda993a3eb69a1b317fc831c8ebcafb5704c390859ebd49f81218c4bbb", size = 225787, upload-time = "2026-08-30T04:40:02.715Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f7/d100f6c348b7669f19119cf890dcd4764623e2233af065586d110e0cd99e/wrapt-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e084558fbd112d2e1e34b0f5c71e45a3405bdad51a17150368a959bcf6697964", size = 226649, upload-time = "2026-08-30T04:40:04.647Z" }, - { url = "https://files.pythonhosted.org/packages/52/c6/3af8df515d5d7e92306957536f3468c6bdfecbe3659f99dbf09a468c2c4c/wrapt-2.4.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e78c947e18fadfd690c9420c30a96d221feeb93fc8f1cc00509b370ac16c3114", size = 206760, upload-time = "2026-08-30T04:40:06.332Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c5/40d355552bd3eb6c5186e26051c19b573d24d7896de42caa7937d6b5ca9f/wrapt-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:08d8378c4514ac8dcc0ace76044cf87a873e6a52b5e6109834c8fb9037f4441b", size = 223467, upload-time = "2026-08-30T04:40:07.829Z" }, - { url = "https://files.pythonhosted.org/packages/40/ab/d198eebdb39f0d7e182e771e590a36673489cd58cebdad8aa273dcf28e04/wrapt-2.4.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:93180c2199784dd6a1075b33f9ed636bd0966821edbece6b3d5379b1c4f0bb7d", size = 205358, upload-time = "2026-08-30T04:40:09.344Z" }, - { url = "https://files.pythonhosted.org/packages/1c/0e/974a60672ad507d39a3d8a1c6351ef37fe65b07240d000ceba5d2b83e9e9/wrapt-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d5e5eb76fb87e62752af751d2dcd9d1cd986b12037d2e1363d109ba716029e8", size = 214654, upload-time = "2026-08-30T04:40:10.923Z" }, - { url = "https://files.pythonhosted.org/packages/cc/5a/8b2db70206db0a4246758e0472ce344cb9636217113ef70640fc8d2ce874/wrapt-2.4.0-cp313-cp313-win32.whl", hash = "sha256:49bb5a572469e0e18163a8ec2aa972135a0929899ecbe627665f274506e1b5b4", size = 91171, upload-time = "2026-08-30T04:40:12.895Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1e/e782b511c680dbe7369c92e7d981484aacca0cda584da1f28a84cd9a8e1a/wrapt-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:b1737f46b1e4a81eb93500a7f2854319e1c7a86e8863fb050b7b4daadd5a4178", size = 96178, upload-time = "2026-08-30T04:40:14.336Z" }, - { url = "https://files.pythonhosted.org/packages/9f/62/095ba31123fa5dd482d6183c05200b061314aabbd5442c010aba4b03ff1c/wrapt-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:f1e9e088094f4895f84ab043e7d59401df137d663efbf1e80c82144882960830", size = 92949, upload-time = "2026-08-30T04:40:15.935Z" }, - { url = "https://files.pythonhosted.org/packages/79/c8/fafe0002f572ced999c792cfe8b05d39269c63d8193d15d25bd828bcad7a/wrapt-2.4.0-py3-none-any.whl", hash = "sha256:18aabd9301d06026f5900538051773d6f87f65ae02cdc60de482df978513dc0a", size = 73713, upload-time = "2026-08-30T04:41:49.805Z" }, +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/42/a6/6375d56c44d590ef24acf0f8f5bf7ed768ff7a510b959306ec412611e90f/wrapt-2.4.1.tar.gz", hash = "sha256:fd6390aab9e8aa40c52eff3c180f098e8d9f5894b1fd4c4fd2c207067b33ed16", size = 164597, upload-time = "2026-09-10T23:12:16.811Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/1f/a2f3225c5ecf522684c1d051aea8ce8253f240e55be75826b787777afd6c/wrapt-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7e86fbc2ac8a363ea04abf631fad82720e16b17a25020f32dbe9b24a2ed2b0e3", size = 98890, upload-time = "2026-09-10T23:10:07.876Z" }, + { url = "https://files.pythonhosted.org/packages/68/6c/eb45660fd4d92cce11ec923f55bb2e647a6c18d30e53734eb07a3c530e31/wrapt-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24389748f0b9d5b67e478fad4fc8b3f1108422ef80716e48eead6cebcebbff08", size = 98742, upload-time = "2026-09-10T23:10:09.236Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4f/17a89a580cb0082e61b8375074d5c9e5d38e4aa83ee19b6174ee472d17c2/wrapt-2.4.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30d11c289b013bf384ff1a1a6553f150d0b855901708a9bef667a5680f8247c9", size = 236301, upload-time = "2026-09-10T23:10:11.039Z" }, + { url = "https://files.pythonhosted.org/packages/01/ca/4700eb008a34bf02de328806ddde15fc84c8d1e65d3dcafb92a935a50319/wrapt-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9356dbb59199a0e4709de35fa4a1ac1a88ef6da99711a397f5b009233faff326", size = 237805, upload-time = "2026-09-10T23:10:12.594Z" }, + { url = "https://files.pythonhosted.org/packages/75/5d/26c1740299b29e190d5f4b4a99eb001401042a9d9ab338e3f8e1ce140ecb/wrapt-2.4.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8342f332dada211f64b74609e332d727b13315e9a83177f7918bf68c59f815f2", size = 217037, upload-time = "2026-09-10T23:10:14.028Z" }, + { url = "https://files.pythonhosted.org/packages/3a/55/ec72991153a2ae8b40238bc44cec7c3ddf7706ef6e2d314b0c6f5c7febce/wrapt-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2f86e328c482bc5383b4eda5094be0bed3617fc3076aa9225ff1a9eb6372de9b", size = 234659, upload-time = "2026-09-10T23:10:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/7cfa1e070dcda76ea56a3e252341cba1cd1e9412baf23cd7adfebf1114e2/wrapt-2.4.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4dc92697444ee380544fbb43c86612d8486529aadf917c22b5524141d4af074c", size = 214590, upload-time = "2026-09-10T23:10:16.744Z" }, + { url = "https://files.pythonhosted.org/packages/79/10/248841cb30107f6f32c53a02662e1c3e0c7c06bea0b8ebfaaee94885dcee/wrapt-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:edd03758a7578526642508b8833d43496fdfba0f64e0025dfca153a7c1777735", size = 225103, upload-time = "2026-09-10T23:10:18.346Z" }, + { url = "https://files.pythonhosted.org/packages/90/02/5b2bf7b35b008a39939a2908e85dba3b867596e535fc9f12ddf3ba1fcaf6/wrapt-2.4.1-cp312-cp312-win32.whl", hash = "sha256:5d83e412665aeb1e854eefbf1564d0d67872d9994b502a0bce96e6ff7f4970b7", size = 93441, upload-time = "2026-09-10T23:10:19.81Z" }, + { url = "https://files.pythonhosted.org/packages/d4/2a/47be56772bfb07ef242d6e924049688e38384af2b2fc99b0f31180988448/wrapt-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:b4e7efdd476ac631a0181551fd9aace844765ea3ce2b5133b194fae4421e8ad0", size = 98808, upload-time = "2026-09-10T23:10:21.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ec/40e4c9735626afd1dc0eeb310310278361f192800503cda0f5b3d8d24db4/wrapt-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:38819761401baa2d11916d7265b82f23265f8fe5a31c431dd7c24a8863c65f88", size = 95240, upload-time = "2026-09-10T23:10:22.39Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1a/9b5aa3c2391aa6d00fffa085c2219e8fc91cd4d2b9b080d2b4e4b6b93f42/wrapt-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:55f36bb1461f93beaf18d818e568b5de343dd8fade7456773d201a38ee723bd3", size = 98597, upload-time = "2026-09-10T23:10:23.723Z" }, + { url = "https://files.pythonhosted.org/packages/fe/07/dc98150c2f9ee5b5fcbd841765e178ea6cc6c43733ab8d1f5181a4fb9f3d/wrapt-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:53e15cd74bd6b84d7fa90b93dda7334d85f4641fae97632de8aa61d268dfd145", size = 98842, upload-time = "2026-09-10T23:10:25.109Z" }, + { url = "https://files.pythonhosted.org/packages/08/c2/0e772a570e8d75c1b3ec45930a3023492fa68223f8f5e3485af4844c10c3/wrapt-2.4.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:be6cdd7121adc89a6f52e3c2f4e26a2d4dcdc1c0fde47e3156234db8939e4cdc", size = 234642, upload-time = "2026-09-10T23:10:26.644Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/4ce4d02dd95ff9ed972a2fc396d04fec636daa5a4ac18cc73a3dc20aa6c3/wrapt-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03b5598edd435373278731d0d53449ce7a9626bc48d5548e4a71124ee3e526a1", size = 235484, upload-time = "2026-09-10T23:10:28.098Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/436ef1df620ef8edd9ef057b4d55a0cd704435ff66852a0ef26cbaa02a58/wrapt-2.4.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fdc997819a6df4c65bdb0a1c5601c98b479ee34cc2f94ffa98a767034ad6366d", size = 214371, upload-time = "2026-09-10T23:10:29.841Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2c/cc5b7503843399087db3106a7cf60d60d0900f69539e96148b9fff116291/wrapt-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3bfc6907ebed560d2d3f677c3b17bf6199679163b6c6e475035c9dca497d1697", size = 232347, upload-time = "2026-09-10T23:10:31.511Z" }, + { url = "https://files.pythonhosted.org/packages/86/fb/17668b1ca572c44b458c09d76064d35f5f57d5ac7629248871604bef816c/wrapt-2.4.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a0c3b217332cf0c4df085fec41c126bf7507d6c1ca0efb3ba8d2b3fd234d4e73", size = 212792, upload-time = "2026-09-10T23:10:32.968Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2b/0c5de10d7259e07c478c44bf2fbe179c6878727d09edf0632b97884801db/wrapt-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a23b89621cfeb3329b1a290596bf402e61d7d5a647a65ca8ea735e48771b71d4", size = 223585, upload-time = "2026-09-10T23:10:34.43Z" }, + { url = "https://files.pythonhosted.org/packages/aa/36/013dce1c687f8f1c87b1e78f403d04c80ae9b2ae77de4ddba16069a3e7d1/wrapt-2.4.1-cp313-cp313-win32.whl", hash = "sha256:bc67d4872af5ab2dc1b88904097b92ac00e7658fdf010dee36807807fe882ac4", size = 93425, upload-time = "2026-09-10T23:10:35.901Z" }, + { url = "https://files.pythonhosted.org/packages/b7/1d/d53bcf5910209a45191c8bc173ff0e00830416547d8038772dc444932ee0/wrapt-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:1fe758b9c2d49138231ec3efabd106fec665f86fec50b3d02d7edd75f08a69ca", size = 98569, upload-time = "2026-09-10T23:10:37.316Z" }, + { url = "https://files.pythonhosted.org/packages/e8/1f/759d0c522918f9dfb620136622d8e1ce619570adda0de8fa2cfbb55cbb86/wrapt-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c974c36e8205255a3947dad9c2fe000431b57dd522946e56c192174a7f92a0f", size = 95272, upload-time = "2026-09-10T23:10:38.657Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a2/edcfc8d9a30375791b775715f8501364588f65b494ec4f6930568a19e765/wrapt-2.4.1-py3-none-any.whl", hash = "sha256:1e84ec5d89a0a07a0ef6bcd343f5c8ecdc95601d71de3058cdc63274e86c193c", size = 75317, upload-time = "2026-09-10T23:12:14.82Z" }, ] [[package]] From 9590d1e5663fcce30d1884809e5f6e45d2504930 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Fri, 11 Sep 2026 08:59:09 +0200 Subject: [PATCH 050/120] Use self as return type --- src/configuration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/configuration.py b/src/configuration.py index 23498e7cb..c6e47322a 100644 --- a/src/configuration.py +++ b/src/configuration.py @@ -1,6 +1,6 @@ """Configuration loader.""" -from typing import Any, Optional +from typing import Any, Optional, Self import yaml @@ -98,7 +98,7 @@ class AppConfig: # pylint: disable=too-many-public-methods _instance = None - def __new__(cls, *args: Any, **kwargs: Any) -> "AppConfig": + def __new__(cls, *args: Any, **kwargs: Any) -> Self: """Create a new instance of the class.""" if not isinstance(cls._instance, cls): cls._instance = super().__new__(cls, *args, **kwargs) From a50b004c6a6ec6d58b571aa5420a35d4124360ab Mon Sep 17 00:00:00 2001 From: Andrej Simurka Date: Thu, 3 Sep 2026 11:57:59 +0200 Subject: [PATCH 051/120] finish OGX runtime naming cleanup --- .github/workflows/e2e_tests.yaml | 7 +- .github/workflows/e2e_tests_providers.yaml | 4 +- .github/workflows/e2e_tests_rhaiis.yaml | 4 +- .gitignore | 4 +- .../lightspeed-stack-integration-test.yaml | 4 +- deploy/ogx/README.md | 23 +- docker-compose.yaml | 22 +- .../ogx-config-merge/ogx-config-merge.md | 8 +- docs/devel_doc/conversations_api.md | 10 +- docs/testing/e2e_testing.md | 8 +- lightspeed-stack.yaml | 8 +- scripts/ogx-entrypoint.sh | 8 +- src/client/ogx.py | 10 +- src/configuration.py | 10 +- src/ogx_configuration.py | 248 +++++------ src/telemetry/configuration_snapshot.py | 18 +- src/utils/conversations.py | 4 +- .../lightspeed/lightspeed-stack.yaml | 2 +- .../manifests/lightspeed/ogx-openai.yaml | 2 +- .../rhoai/manifests/lightspeed/ogx-prow.yaml | 4 +- tests/e2e-prow/rhoai/pipeline-konflux.sh | 34 +- .../rhoai/pipeline-services-konflux.sh | 2 +- tests/e2e-prow/rhoai/pipeline-services.sh | 4 +- tests/e2e-prow/rhoai/pipeline.sh | 24 +- tests/e2e-prow/rhoai/scripts/e2e-ops.sh | 54 +-- .../lightspeed-stack-shields-empty.yaml | 2 +- ...speed-stack-shields-override-disabled.yaml | 2 +- .../lightspeed-stack-shields.yaml | 2 +- .../lightspeed-stack-authorized.yaml | 2 +- .../server-mode/lightspeed-stack-default.yaml | 2 +- .../lightspeed-stack-degraded.yaml | 2 +- .../lightspeed-stack-mcp-api-auth.yaml | 2 +- .../lightspeed-stack-mcp-client-auth.yaml | 2 +- .../lightspeed-stack-mcp-file-auth.yaml | 2 +- .../lightspeed-stack-mcp-invalid.yaml | 2 +- .../lightspeed-stack-mcp-kubernetes-auth.yaml | 2 +- .../lightspeed-stack-mcp-oauth-auth.yaml | 2 +- .../server-mode/lightspeed-stack-mcp.yaml | 2 +- .../lightspeed-stack-negative.yaml | 2 +- .../server-mode/lightspeed-stack-rbac.yaml | 2 +- .../lightspeed-stack-rh-identity.yaml | 2 +- .../server-mode/lightspeed-stack-rhelai.yaml | 2 +- .../server-mode/lightspeed-stack-rhoai.yaml | 2 +- .../lightspeed-stack-shields-empty.yaml | 4 +- ...speed-stack-shields-override-disabled.yaml | 4 +- .../server-mode/lightspeed-stack-shields.yaml | 4 +- .../lightspeed-stack-skills-directory.yaml | 2 +- .../server-mode/lightspeed-stack-skills.yaml | 2 +- .../server-mode/lightspeed-stack-tls.yaml | 2 +- .../server-mode/lightspeed-stack.yaml | 2 +- .../features/degraded_mode_startup.feature | 18 +- tests/e2e/features/environment.py | 60 ++- tests/e2e/features/ogx_disrupted.feature | 92 ++--- tests/e2e/features/okp_rag.feature | 2 +- tests/e2e/features/proxy.feature | 36 +- tests/e2e/features/shields.feature | 2 +- tests/e2e/features/steps/common.py | 34 +- tests/e2e/features/steps/health.py | 48 +-- tests/e2e/features/steps/proxy.py | 96 ++--- tests/e2e/features/steps/shields.py | 4 +- tests/e2e/features/steps/tls.py | 66 +-- tests/e2e/features/tls-ca.feature | 28 +- tests/e2e/features/tls-mtls.feature | 28 +- tests/e2e/features/tls-tlsv13.feature | 20 +- tests/e2e/features/unified-mode-boot.feature | 12 +- .../e2e/features/unified-mode-legacy.feature | 2 +- .../features/unified-mode-migration.feature | 6 +- .../features/unified-mode-synthesis.feature | 2 +- .../features/unified-mode-validation.feature | 2 +- tests/e2e/utils/ogx_config_utils.py | 66 +-- tests/e2e/utils/ogx_prow_utils.py | 33 +- tests/e2e/utils/ogx_utils.py | 16 +- tests/e2e/utils/prow_utils.py | 21 +- tests/e2e/utils/utils.py | 30 +- tests/integration/test_configuration.py | 10 +- .../models/config/test_ogx_configuration.py | 94 ++--- tests/unit/telemetry/conftest.py | 8 +- .../telemetry/test_configuration_snapshot.py | 52 +-- tests/unit/test_lightspeed_stack.py | 2 +- tests/unit/test_ogx_configuration.py | 384 +++++++++--------- tests/unit/test_ogx_synthesize.py | 118 +++--- .../unit/utils/dumpers/test_models_dumper.py | 4 +- tests/unit/utils/test_compaction.py | 4 +- tests/unit/utils/test_token_estimator.py | 6 +- 84 files changed, 995 insertions(+), 993 deletions(-) diff --git a/.github/workflows/e2e_tests.yaml b/.github/workflows/e2e_tests.yaml index 4bfe69e09..5e14d19cf 100644 --- a/.github/workflows/e2e_tests.yaml +++ b/.github/workflows/e2e_tests.yaml @@ -54,8 +54,7 @@ jobs: E2E_OPENAI_MODEL: ${{ vars.E2E_OPENAI_MODEL }} E2E_DEPLOYMENT_MODE: ${{ matrix.mode }} FAISS_VECTOR_STORE_ID: ${{ vars.FAISS_VECTOR_STORE_ID }} - # Override via repo Actions variable E2E_LLAMA_HOSTNAME; default matches server-mode lightspeed-stack.yaml - E2E_LLAMA_HOSTNAME: ${{ vars.E2E_LLAMA_HOSTNAME || 'llama-stack' }} + E2E_OGX_HOSTNAME: ogx steps: - uses: actions/checkout@v7 @@ -289,8 +288,8 @@ jobs: echo "=== Test failure logs ===" if [ "${{ matrix.mode }}" == "server" ]; then - echo "=== llama-stack logs ===" - docker compose logs llama-stack + echo "=== OGX logs ===" + docker compose logs ogx echo "" echo "=== lightspeed-stack logs ===" docker compose logs lightspeed-stack diff --git a/.github/workflows/e2e_tests_providers.yaml b/.github/workflows/e2e_tests_providers.yaml index 3d988e2b3..284ccb87f 100644 --- a/.github/workflows/e2e_tests_providers.yaml +++ b/.github/workflows/e2e_tests_providers.yaml @@ -355,8 +355,8 @@ jobs: echo "=== Test failure logs ===" if [ "${{ matrix.mode }}" == "server" ]; then - echo "=== llama-stack logs ===" - docker compose logs llama-stack + echo "=== OGX logs ===" + docker compose logs ogx echo "" echo "=== lightspeed-stack logs ===" docker compose logs lightspeed-stack diff --git a/.github/workflows/e2e_tests_rhaiis.yaml b/.github/workflows/e2e_tests_rhaiis.yaml index da2fe0497..0b1c81c2b 100644 --- a/.github/workflows/e2e_tests_rhaiis.yaml +++ b/.github/workflows/e2e_tests_rhaiis.yaml @@ -249,8 +249,8 @@ jobs: echo "=== Test failure logs ===" if [ "${{ matrix.mode }}" == "server" ]; then - echo "=== llama-stack logs ===" - docker compose logs llama-stack + echo "=== OGX logs ===" + docker compose logs ogx echo "" echo "=== lightspeed-stack logs ===" docker compose logs lightspeed-stack diff --git a/.gitignore b/.gitignore index 15f06684a..3d7a1f08f 100644 --- a/.gitignore +++ b/.gitignore @@ -183,7 +183,7 @@ dev/ # VSCode .vscode/ -# Llama related - when running the stack as lib client +# OGX related - when running the stack as lib client .llama # Database files @@ -195,7 +195,7 @@ requirements.*.backup # Local run files local-run.yaml -# Synthesized Llama Stack run.yaml written by unified library mode (LCORE-2336) +# Synthesized OGX run.yaml written by unified library mode (LCORE-2336) .generated/ # Sisyphus planning files diff --git a/.tekton/integration-tests/pipeline/lightspeed-stack-integration-test.yaml b/.tekton/integration-tests/pipeline/lightspeed-stack-integration-test.yaml index c3060e079..34a5e7735 100644 --- a/.tekton/integration-tests/pipeline/lightspeed-stack-integration-test.yaml +++ b/.tekton/integration-tests/pipeline/lightspeed-stack-integration-test.yaml @@ -15,7 +15,7 @@ spec: default: '{"components": [{"name":"lightspeed-stack-0-8", "containerImage": "quay.io/example/lightspeed-stack-0-8:latest"}]}' type: string - name: llama-stack-image - description: 'Llama Stack runs from source on UBI (init container clones repo and installs deps). Kept for logging/backwards compatibility.' + description: 'OGX runs from source on UBI (init container clones repo and installs deps). Parameter name kept for backwards compatibility.' default: 'run-from-source (UBI)' type: string - name: test-name @@ -81,7 +81,7 @@ spec: - name: instanceType value: "m5.large" - name: get-stack-images - description: Extract lightspeed-stack image and commit from SNAPSHOT (Llama Stack runs from source in-pod) + description: Extract lightspeed-stack image and commit from SNAPSHOT (OGX runs from source in-pod) runAfter: - provision-cluster params: diff --git a/deploy/ogx/README.md b/deploy/ogx/README.md index 5554a1aca..27749f138 100644 --- a/deploy/ogx/README.md +++ b/deploy/ogx/README.md @@ -1,9 +1,9 @@ -# Llama Stack container image +# OGX container image -`test.containerfile` builds the Llama Stack server image used by -`docker-compose.yaml` (server mode, e.g. for the e2e suite). Besides the -Llama Stack distribution itself, the image bundles the pieces needed to -generate its run configuration at container start: +`test.containerfile` builds the OGX server image used by `docker-compose.yaml` +(server mode, e.g. for the e2e suite). Besides the OGX distribution itself, the +image bundles the pieces needed to generate its run configuration at container +start: - `/opt/app-root/ogx_configuration.py` — the config-generation script (copied from `src/ogx_configuration.py`). @@ -21,11 +21,12 @@ shape: - **Unified mode** — the `lightspeed-stack.yaml` carries a *synthesis input* (a non-empty `inference.providers` or `vector_store.providers`, - or a `llama_stack.config` block). The full `run.yaml` is synthesized - from it; no external `run.yaml` mount is needed. + or an `ogx.config` block). The full `run.yaml` is synthesized from it; + no external `run.yaml` mount is needed. - **Legacy mode** — no synthesis input present. The mounted `run.yaml` - (`$LLAMA_STACK_CONFIG`, default `/opt/app-root/run.yaml`) is enriched - with lightspeed dynamic values (BYOK RAG, Solr/OKP, Azure Entra ID). + (`$OGX_CONFIG`, with deprecated fallback `$LLAMA_STACK_CONFIG`, default + `/opt/app-root/run.yaml`) is enriched with lightspeed dynamic values + (BYOK RAG, Solr/OKP, Azure Entra ID). The repository `docker-compose.yaml` mounts both files and works for either mode — with a unified `lightspeed-stack.yaml` the `run.yaml` @@ -33,7 +34,7 @@ mount is simply ignored. A unified-only deployment needs just: ```yaml services: - llama-stack: + ogx: build: context: . dockerfile: deploy/ogx/test.containerfile @@ -48,5 +49,5 @@ services: The compose file also mounts host copies of the script, the baseline data directory, and the entrypoint over their baked-in counterparts, so `docker compose up` picks up local changes to any of them without an -image rebuild. Rebuild (`docker compose build llama-stack`) when +image rebuild. Rebuild (`docker compose build ogx`) when dependencies (`pyproject.toml` / `uv.lock`) or the providers change. diff --git a/docker-compose.yaml b/docker-compose.yaml index 16dcda1bc..6dff730b7 100755 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,11 +1,11 @@ services: # Red Hat OGX distribution with FAISS - llama-stack: + ogx: build: context: . dockerfile: deploy/ogx/test.containerfile platform: linux/amd64 - container_name: llama-stack + container_name: ogx ports: - "8321:8321" # Expose OGX on 8321 (adjust if needed) depends_on: @@ -14,7 +14,7 @@ services: volumes: # Used in legacy mode only: with a unified lightspeed-stack.yaml (one # carrying inference.providers / vector_store.providers / a - # llama_stack.config block) the entrypoint synthesizes the run config + # ogx.config block) the entrypoint synthesizes the run config # from lightspeed-stack.yaml and this mount is ignored - ./run.yaml:/opt/app-root/run.yaml:z # Host copies so `docker compose up` picks up script changes without rebuilding OGX @@ -24,7 +24,7 @@ services: - ${GCP_KEYS_PATH:-./tmp/.gcp-keys-dummy}:/opt/app-root/.gcp-keys:ro - ./lightspeed-stack.yaml:/opt/app-root/lightspeed-stack.yaml:ro,z # Writable OGX storage (rag/ work copy is re-seeded by entrypoint each start) - - llama-storage:/opt/app-root/src/.llama/storage + - ogx-storage:/opt/app-root/src/.llama/storage # Read-only e2e FAISS fixtures — never mount these as the live KV_RAG_PATH - ./tests/e2e/rag:/opt/app-root/src/.llama/storage/.e2e-rag-seed:ro,z - mock-tls-certs:/certs:ro @@ -80,8 +80,8 @@ services: - SOLR_CONTENT_FIELD=${SOLR_CONTENT_FIELD:-} - SOLR_EMBEDDING_MODEL=${SOLR_EMBEDDING_MODEL:-} - SOLR_EMBEDDING_DIM=${SOLR_EMBEDDING_DIM:-} - #required for enrichment - - E2E_LLAMA_HOSTNAME=${E2E_LLAMA_HOSTNAME:-llama-stack} + # Required for enrichment; E2E_LLAMA_HOSTNAME is the external/GitHub Actions name + - E2E_OGX_HOSTNAME=${E2E_OGX_HOSTNAME:-${E2E_LLAMA_HOSTNAME:-ogx}} networks: - lightspeednet healthcheck: @@ -111,9 +111,9 @@ services: - CLIENT_SECRET=${CLIENT_SECRET:-} # FAISS vector store ID (used by inline RAG config) - FAISS_VECTOR_STORE_ID=${FAISS_VECTOR_STORE_ID:-} - # Substituted in mounted lightspeed-stack.yaml (llama_stack.url); GitHub Actions sets via vars - - E2E_LLAMA_HOSTNAME=${E2E_LLAMA_HOSTNAME:-llama-stack} - - E2E_LLAMA_PORT=${E2E_LLAMA_PORT:-8321} + # Substituted in mounted lightspeed-stack.yaml (ogx.url); GitHub Actions sets E2E_LLAMA_HOSTNAME + - E2E_OGX_HOSTNAME=${E2E_OGX_HOSTNAME:-${E2E_LLAMA_HOSTNAME:-ogx}} + - E2E_OGX_PORT=${E2E_OGX_PORT:-${E2E_LLAMA_PORT:-8321}} # OpenTelemetry configuration (tracing disabled by default) - OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT:-} - OTEL_EXPORTER_OTLP_PROTOCOL=${OTEL_EXPORTER_OTLP_PROTOCOL:-} @@ -121,7 +121,7 @@ services: - OTEL_ANONYMIZATION_SECRET=${OTEL_ANONYMIZATION_SECRET:-lightspeed-stack-otel-anonymization-dev-default} - OTEL_SDK_DISABLED=${OTEL_SDK_DISABLED:-true} depends_on: - llama-stack: + ogx: condition: service_healthy mock-mcp: condition: service_healthy @@ -186,7 +186,7 @@ services: volumes: - llama-storage: + ogx-storage: mock-tls-certs: networks: diff --git a/docs/design/ogx-config-merge/ogx-config-merge.md b/docs/design/ogx-config-merge/ogx-config-merge.md index 77d27ef59..4a63bc48b 100644 --- a/docs/design/ogx-config-merge/ogx-config-merge.md +++ b/docs/design/ogx-config-merge/ogx-config-merge.md @@ -351,7 +351,7 @@ None at the REST API surface. Internal API additions in - `migrate_config_dumb(run_yaml_path, lightspeed_yaml_path, output_path) -> None` — dumb-mode migration (lossless round-trip). - `deep_merge_list_replace(base, overlay) -> dict` — merge helper. -- `apply_high_level_inference(ls_config, inference)` — high-level expansion. +- `apply_high_level_inference(ogx_config, inference)` — high-level expansion. - `load_default_baseline() -> dict` — loads `src/data/default_run.yaml`. CLI additions in `src/lightspeed_stack.py`: @@ -474,11 +474,11 @@ not have had a full release with a working migration path. Releases: 3. Run `dedupe_providers_vector_io` on the baseline. 4. Apply existing enrichment: `enrich_byok_rag`, `enrich_solr` (Azure Entra ID intentionally stays separate because it's a `.env` - side-effect, not an `ls_config` mutation). + side-effect, not an `ogx_config` mutation). 5. If top-level `inference.providers` is non-empty → - `apply_high_level_inference(ls_config, lcs_config["inference"])`. + `apply_high_level_inference(ogx_config, lcs_config["inference"])`. 6. If `unified` and `unified.native_override` non-empty → - `deep_merge_list_replace(ls_config, native_override)`. + `deep_merge_list_replace(ogx_config, native_override)`. 7. `dedupe_providers_vector_io` again for good measure. 8. Return the final dict. diff --git a/docs/devel_doc/conversations_api.md b/docs/devel_doc/conversations_api.md index 23f94ab6b..2134df6b7 100644 --- a/docs/devel_doc/conversations_api.md +++ b/docs/devel_doc/conversations_api.md @@ -115,9 +115,9 @@ When a user makes a query **without** providing a `conversation_id`: ```python # No conversation_id provided - create a new conversation first conversation = await client.conversations.create(metadata={}) -llama_stack_conv_id = conversation.id +ogx_conv_id = conversation.id # Store the normalized version -conversation_id = normalize_conversation_id(llama_stack_conv_id) +conversation_id = normalize_conversation_id(ogx_conv_id) # Use the conversation in responses.create() response = await client.responses.create( @@ -125,7 +125,7 @@ response = await client.responses.create( model=model_id, instructions=system_prompt, store=True, - conversation=llama_stack_conv_id, # Use OGX format + conversation=ogx_conv_id, # Use OGX format # ... other parameters ) ``` @@ -151,7 +151,7 @@ ogx_conv_id = to_ogx_conversation_id(conversation_id) response = await client.responses.create( input=input_text, model=model_id, - conversation=llama_stack_conv_id, # Existing conversation + conversation=ogx_conv_id, # Existing conversation # ... other parameters ) ``` @@ -166,7 +166,7 @@ Conversations are stored in **two databases**: - `openai_conversations`: Stores conversation metadata - `conversation_items`: Stores individual messages/turns in conversations -**Configuration (in `config/llama_stack_client_config.yaml`):** +**Configuration (in OGX `run.yaml` / library client config):** ```yaml storage: stores: diff --git a/docs/testing/e2e_testing.md b/docs/testing/e2e_testing.md index f2666134c..50d442bec 100644 --- a/docs/testing/e2e_testing.md +++ b/docs/testing/e2e_testing.md @@ -142,10 +142,10 @@ uv run behave tests/e2e/features/health.feature --tags=-skip-in-library-mode | `E2E_DEPLOYMENT_MODE` | `server` | `server` or `library`. Drives config paths and which scenarios run (e.g. `@skip-in-library-mode`). | | `E2E_LSC_HOSTNAME` | `localhost` | Host of the Lightspeed Core Stack API. | | `E2E_LSC_PORT` | `8080` | Port of the Lightspeed Core Stack API. | -| `E2E_LLAMA_HOSTNAME` | `localhost` | Host of the OGX service (server mode). | -| `E2E_LLAMA_PORT` | `8321` | Port of the OGX service. | -| `E2E_LLAMA_STACK_URL` | — | Full base URL for OGX (overrides host/port if set). Used by shield helpers. | -| `E2E_LLAMA_STACK_API_KEY` | `xyzzy` | API key for OGX client (e.g. shield API). | +| `E2E_OGX_HOSTNAME` | `localhost` | Host of the OGX service (server mode). GitHub Actions repo variable: `E2E_LLAMA_HOSTNAME` (compose maps it here). | +| `E2E_OGX_PORT` | `8321` | Port of the OGX service. | +| `E2E_OGX_STACK_URL` | — | Full base URL for OGX (overrides host/port if set). Used by shield helpers. | +| `E2E_OGX_STACK_API_KEY` | `xyzzy` | API key for OGX client (e.g. shield API). | | `E2E_DEFAULT_MODEL_OVERRIDE` | — | Override default LLM model id (e.g. `gpt-4o-mini`). | | `E2E_DEFAULT_PROVIDER_OVERRIDE` | — | Override default provider id (e.g. `openai`). | | `FAISS_VECTOR_STORE_ID` | — | Vector store id for FAISS-related scenarios. | diff --git a/lightspeed-stack.yaml b/lightspeed-stack.yaml index 62af987bb..c784d14c1 100644 --- a/lightspeed-stack.yaml +++ b/lightspeed-stack.yaml @@ -7,11 +7,11 @@ service: workers: 1 color_log: true access_log: true -# llama_stack configuration +# ogx configuration # When using 'make run', a container is ALWAYS launched at http://localhost:8321 (hardcoded in Makefile). -# This llama_stack section controls where lightspeed-core connects to OGX. -# To use a different port: override with 'make run LLAMA_STACK_PORT=' and update the url below, -# or run ogx manually and don't use 'make run'. +# This ogx section controls where lightspeed-core connects to OGX. +# To use a different port: override with 'make run OGX_PORT=' and update the url below, +# or run OGX manually and don't use 'make run'. ogx: use_as_library_client: false url: http://localhost:8321 diff --git a/scripts/ogx-entrypoint.sh b/scripts/ogx-entrypoint.sh index b510ec040..81a409b10 100755 --- a/scripts/ogx-entrypoint.sh +++ b/scripts/ogx-entrypoint.sh @@ -6,12 +6,12 @@ # configuration shape: # - unified mode: the lightspeed config carries a synthesis input (a # non-empty inference.providers or vector_store.providers, or a -# llama_stack.config / ogx.config block). The full run.yaml is synthesized +# ogx.config block (deprecated alias: llama_stack.config). The full run.yaml is synthesized # from it — no external run.yaml mount is needed, and $OGX_CONFIG / -# $LLAMA_STACK_CONFIG is ignored. The shipped default baseline is read from +# $LLAMA_STACK_CONFIG (deprecated) is ignored. The shipped default baseline is read from # /opt/app-root/data/default_run.yaml. -# - legacy mode: the mounted run.yaml ($OGX_CONFIG, falling back to -# $LLAMA_STACK_CONFIG) is enriched with lightspeed dynamic values +# - legacy mode: the mounted run.yaml ($OGX_CONFIG, falling back to the +# deprecated $LLAMA_STACK_CONFIG) is enriched with lightspeed dynamic values # (BYOK RAG, Solr/OKP, Azure Entra ID). set -e diff --git a/src/client/ogx.py b/src/client/ogx.py index 3fcf6a51d..f6e54f30d 100644 --- a/src/client/ogx.py +++ b/src/client/ogx.py @@ -159,7 +159,7 @@ def _enrich_library_config(self, input_config_path: str) -> str: """Enrich OGX config with BYOK RAG and OKP Solr settings.""" try: with open(input_config_path, encoding="utf-8") as f: - ls_config = yaml.safe_load(f) + ogx_config = yaml.safe_load(f) except (OSError, yaml.YAMLError) as e: logger.warning("Failed to read OGX config: %s", e) return input_config_path @@ -167,26 +167,26 @@ def _enrich_library_config(self, input_config_path: str) -> str: config = configuration.configuration # Enrichment: BYOK RAG - enrich_byok_rag(ls_config, [s.model_dump() for s in config.rag.byok.stores]) + enrich_byok_rag(ogx_config, [s.model_dump() for s in config.rag.byok.stores]) # Enrichment: Solr - enabled when "okp" appears in either inline or tool list rag_config_for_solr = { "inline": config.rag.retrieval.inline.sources, "tool": config.rag.retrieval.tool.sources, } - enrich_solr(ls_config, rag_config_for_solr, config.rag.okp.model_dump()) + enrich_solr(ogx_config, rag_config_for_solr, config.rag.okp.model_dump()) # Enrichment: Azure Entra ID deferred auth entra_id_config = ( config.azure_entra_id.model_dump() if config.azure_entra_id else None ) - enrich_azure_entra_id_inference(ls_config, entra_id_config) + enrich_azure_entra_id_inference(ogx_config, entra_id_config) enriched_path = os.path.join(tempfile.gettempdir(), "ogx_enriched_config.yaml") try: with open(enriched_path, "w", encoding="utf-8") as f: - yaml.dump(ls_config, f, Dumper=YamlDumper, default_flow_style=False) + yaml.dump(ogx_config, f, Dumper=YamlDumper, default_flow_style=False) logger.info("Wrote enriched OGX config to %s", enriched_path) return enriched_path except OSError as e: diff --git a/src/configuration.py b/src/configuration.py index 23498e7cb..3e92607b0 100644 --- a/src/configuration.py +++ b/src/configuration.py @@ -73,14 +73,14 @@ def replace_env_vars_preserving_native_override( ogx_section = config_dict.get("ogx") if ogx_section is None: ogx_section = config_dict.get("llama_stack") - ls_config = ogx_section.get("config") if isinstance(ogx_section, dict) else None - if not (isinstance(ls_config, dict) and "native_override" in ls_config): + ogx_config = ogx_section.get("config") if isinstance(ogx_section, dict) else None + if not (isinstance(ogx_config, dict) and "native_override" in ogx_config): return replace_env_vars(config_dict) - raw_override = ls_config["native_override"] - ls_config["native_override"] = {} # keep secrets out of env resolution + raw_override = ogx_config["native_override"] + ogx_config["native_override"] = {} # keep secrets out of env resolution resolved = replace_env_vars(config_dict) - ls_config["native_override"] = raw_override # restore source dict if reused + ogx_config["native_override"] = raw_override # restore source dict if reused resolved_ogx = (resolved.get("ogx") or resolved.get("llama_stack") or {}).get( "config" ) diff --git a/src/ogx_configuration.py b/src/ogx_configuration.py index ea6309ecf..d5092bc41 100644 --- a/src/ogx_configuration.py +++ b/src/ogx_configuration.py @@ -155,7 +155,7 @@ def increase_indent(self, flow: bool = False, indentless: bool = False) -> None: def enrich_azure_entra_id_inference( - ls_config: dict[str, Any], + ogx_config: dict[str, Any], azure_entra_id: Optional[dict[str, Any]], ) -> None: """Enrich remote::azure inference provider for Entra ID authentication. @@ -164,7 +164,7 @@ def enrich_azure_entra_id_inference( with model_validation=false to defer model validation to runtime. Parameters: - ls_config (dict[str, Any]): Mutable OGX configuration dictionary to update. + ogx_config (dict[str, Any]): Mutable OGX configuration dictionary to update. azure_entra_id (Optional[dict[str, Any]]): Lightspeed azure_entra_id block, or None. @@ -174,7 +174,7 @@ def enrich_azure_entra_id_inference( if azure_entra_id is None: return - inference_providers = ls_config.get("providers", {}).get("inference", []) + inference_providers = ogx_config.get("providers", {}).get("inference", []) for provider in inference_providers: if provider.get("provider_type") != "remote::azure": @@ -215,18 +215,18 @@ def _dedupe_vector_io_list(entries: list[Any]) -> list[dict[str, Any]]: return out -def dedupe_providers_vector_io(ls_config: dict[str, Any]) -> None: +def dedupe_providers_vector_io(ogx_config: dict[str, Any]) -> None: """Collapse ``providers.vector_io`` to one entry per ``provider_id``.""" - if "providers" not in ls_config or "vector_io" not in ls_config["providers"]: + if "providers" not in ogx_config or "vector_io" not in ogx_config["providers"]: return - raw = ls_config["providers"]["vector_io"] + raw = ogx_config["providers"]["vector_io"] if not isinstance(raw, list): return - ls_config["providers"]["vector_io"] = _dedupe_vector_io_list(raw) + ogx_config["providers"]["vector_io"] = _dedupe_vector_io_list(raw) def construct_storage_backends_section( - ls_config: dict[str, Any], byok_rag: list[dict[str, Any]] + ogx_config: dict[str, Any], byok_rag: list[dict[str, Any]] ) -> dict[str, Any]: """Construct storage.backends section in OGX configuration file. @@ -235,7 +235,7 @@ def construct_storage_backends_section( Parameters: ---------- - ls_config (dict[str, Any]): Existing OGX configuration mapping. + ogx_config (dict[str, Any]): Existing OGX configuration mapping. byok_rag (list[dict[str, Any]]): List of BYOK RAG definitions. Returns: @@ -245,8 +245,8 @@ def construct_storage_backends_section( output: dict[str, Any] = {} # preserve existing backends - if "storage" in ls_config and "backends" in ls_config["storage"]: - output = ls_config["storage"]["backends"].copy() + if "storage" in ogx_config and "backends" in ogx_config["storage"]: + output = ogx_config["storage"]["backends"].copy() # add new backends for each BYOK RAG (skip types that don't need one) added = 0 @@ -273,7 +273,7 @@ def construct_storage_backends_section( def construct_vector_stores_section( - ls_config: dict[str, Any], byok_rag: list[dict[str, Any]] + ogx_config: dict[str, Any], byok_rag: list[dict[str, Any]] ) -> list[dict[str, Any]]: """Construct registered_resources.vector_stores section in OGX config. @@ -281,7 +281,7 @@ def construct_vector_stores_section( Parameters: ---------- - ls_config (dict[str, Any]): Existing OGX configuration mapping + ogx_config (dict[str, Any]): Existing OGX configuration mapping used as the base; existing `registered_resources.vector_stores` entries are preserved if present. byok_rag (list[dict[str, Any]]): List of BYOK RAG definitions to be added to @@ -299,9 +299,9 @@ def construct_vector_stores_section( output = [] # fill-in existing vector_stores entries from registered_resources - if "registered_resources" in ls_config: - if "vector_stores" in ls_config["registered_resources"]: - output = ls_config["registered_resources"]["vector_stores"].copy() + if "registered_resources" in ogx_config: + if "vector_stores" in ogx_config["registered_resources"]: + output = ogx_config["registered_resources"]["vector_stores"].copy() # append new vector_stores entries, skipping duplicates # Resolve ${env.VAR} patterns so comparisons work when existing entries @@ -342,7 +342,7 @@ def construct_vector_stores_section( def construct_models_section( - ls_config: dict[str, Any], byok_rag: list[dict[str, Any]] + ogx_config: dict[str, Any], byok_rag: list[dict[str, Any]] ) -> list[dict[str, Any]]: """Construct registered_resources.models section with embedding models. @@ -350,7 +350,7 @@ def construct_models_section( Parameters: ---------- - ls_config (dict[str, Any]): Existing OGX configuration mapping. + ogx_config (dict[str, Any]): Existing OGX configuration mapping. byok_rag (list[dict[str, Any]]): List of BYOK RAG definitions. Returns: @@ -360,9 +360,9 @@ def construct_models_section( output: list[dict[str, Any]] = [] # preserve existing models - if "registered_resources" in ls_config: - if "models" in ls_config["registered_resources"]: - output = ls_config["registered_resources"]["models"].copy() + if "registered_resources" in ogx_config: + if "models" in ogx_config["registered_resources"]: + output = ogx_config["registered_resources"]["models"].copy() # add embedding models for each BYOK RAG for brag in byok_rag: @@ -448,7 +448,7 @@ def _build_vector_io_config( def construct_vector_io_providers_section( - ls_config: dict[str, Any], byok_rag: list[dict[str, Any]] + ogx_config: dict[str, Any], byok_rag: list[dict[str, Any]] ) -> list[dict[str, Any]]: """Construct providers/vector_io section in OGX configuration file. @@ -458,7 +458,7 @@ def construct_vector_io_providers_section( Parameters: ---------- - ls_config (dict[str, Any]): Existing OGX configuration + ogx_config (dict[str, Any]): Existing OGX configuration dictionary; if it contains providers.vector_io, those entries are used as the starting list. byok_rag (list[dict[str, Any]]): List of BYOK RAG specifications to convert @@ -474,8 +474,8 @@ def construct_vector_io_providers_section( """ output: list[dict[str, Any]] = [] - if "providers" in ls_config and "vector_io" in ls_config["providers"]: - raw = ls_config["providers"]["vector_io"] + if "providers" in ogx_config and "vector_io" in ogx_config["providers"]: + raw = ogx_config["providers"]["vector_io"] if isinstance(raw, list): output = _dedupe_vector_io_list(raw) else: @@ -515,44 +515,44 @@ def construct_vector_io_providers_section( return output -def enrich_byok_rag(ls_config: dict[str, Any], byok_rag: list[dict[str, Any]]) -> None: +def enrich_byok_rag(ogx_config: dict[str, Any], byok_rag: list[dict[str, Any]]) -> None: """Enrich OGX config with BYOK RAG settings. Args: - ls_config: OGX configuration dict (modified in place) + ogx_config: OGX configuration dict (modified in place) byok_rag: List of BYOK RAG configurations """ if len(byok_rag) == 0: logger.info("BYOK RAG is not configured: skipping") - dedupe_providers_vector_io(ls_config) + dedupe_providers_vector_io(ogx_config) return logger.info("Enriching OGX config with BYOK RAG") # Add storage backends - if "storage" not in ls_config: - ls_config["storage"] = {} - ls_config["storage"]["backends"] = construct_storage_backends_section( - ls_config, byok_rag + if "storage" not in ogx_config: + ogx_config["storage"] = {} + ogx_config["storage"]["backends"] = construct_storage_backends_section( + ogx_config, byok_rag ) # Add vector_io providers - if "providers" not in ls_config: - ls_config["providers"] = {} - ls_config["providers"]["vector_io"] = construct_vector_io_providers_section( - ls_config, byok_rag + if "providers" not in ogx_config: + ogx_config["providers"] = {} + ogx_config["providers"]["vector_io"] = construct_vector_io_providers_section( + ogx_config, byok_rag ) # Add registered vector stores - if "registered_resources" not in ls_config: - ls_config["registered_resources"] = {} - ls_config["registered_resources"]["vector_stores"] = ( - construct_vector_stores_section(ls_config, byok_rag) + if "registered_resources" not in ogx_config: + ogx_config["registered_resources"] = {} + ogx_config["registered_resources"]["vector_stores"] = ( + construct_vector_stores_section(ogx_config, byok_rag) ) # Add embedding models - ls_config["registered_resources"]["models"] = construct_models_section( - ls_config, byok_rag + ogx_config["registered_resources"]["models"] = construct_models_section( + ogx_config, byok_rag ) @@ -585,7 +585,7 @@ def _vector_store_provider_by_id( def _upsert_vsprov_embedding_model( - ls_config: dict[str, Any], + ogx_config: dict[str, Any], provider_id: str, embedding_model: str, embedding_dimension: int, @@ -598,13 +598,13 @@ def _upsert_vsprov_embedding_model( ``model_id`` already exists. Parameters: - ls_config: OGX configuration modified in place. + ogx_config: OGX configuration modified in place. provider_id: Dynamic provider id used to name the model row. embedding_model: Configured embedding model path or id. embedding_dimension: Embedding vector dimensionality (required on validated ``vector_store.providers`` entries). """ - models = ls_config.setdefault("registered_resources", {}).setdefault("models", []) + models = ogx_config.setdefault("registered_resources", {}).setdefault("models", []) model_id = f"vsprov_{provider_id}_embedding" provider_model_id = embedding_model.removeprefix("sentence-transformers/") entry = { @@ -693,18 +693,18 @@ def _replace_or_append_vector_io( def _apply_vector_stores_defaults( - ls_config: dict[str, Any], designated: dict[str, Any] + ogx_config: dict[str, Any], designated: dict[str, Any] ) -> None: """Write vector_stores.default_* from the designated provider entry. Parameters: - ls_config: OGX configuration modified in place. + ogx_config: OGX configuration modified in place. designated: Provider entry selected by ``vector_store.default_provider``. """ - vector_stores = ls_config.get("vector_stores") + vector_stores = ogx_config.get("vector_stores") if not isinstance(vector_stores, dict): vector_stores = {} - ls_config["vector_stores"] = vector_stores + ogx_config["vector_stores"] = vector_stores provider_id = str(designated["id"]).strip() vector_stores["default_provider_id"] = provider_id # Match _upsert_vsprov_embedding_model model_id; OGX validates @@ -721,7 +721,7 @@ def _enrich_one_vector_store_provider( backends: dict[str, Any], vector_io: list[Any], existing_ids: set[str], - ls_config: dict[str, Any], + ogx_config: dict[str, Any], ) -> None: """Enrich LS config for a single ``vector_store.providers`` entry. @@ -730,11 +730,11 @@ def _enrich_one_vector_store_provider( backends: ``storage.backends`` map (modified in place for faiss). vector_io: ``providers.vector_io`` list (modified in place). existing_ids: Known ``provider_id`` values already in ``vector_io``. - ls_config: Full OGX config (for embedding model registration). + ogx_config: Full OGX config (for embedding model registration). """ provider_id = str(entry["id"]).strip() product_type = entry["type"] - ls_type = BACKEND_TO_PROVIDER_TYPE[product_type] + ogx_provider_type = BACKEND_TO_PROVIDER_TYPE[product_type] extra_fields, backend_name, backend_entry = _vsprov_fields_and_backend( product_type, provider_id, entry.get("config") or {} ) @@ -746,8 +746,10 @@ def _enrich_one_vector_store_provider( existing_ids, { "provider_id": provider_id, - "provider_type": ls_type, - "config": _build_vector_io_config(ls_type, backend_name, extra_fields), + "provider_type": ogx_provider_type, + "config": _build_vector_io_config( + ogx_provider_type, backend_name, extra_fields + ), }, ) @@ -755,7 +757,7 @@ def _enrich_one_vector_store_provider( embedding_dimension = entry.get("embedding_dimension") if embedding_model and embedding_dimension is not None: _upsert_vsprov_embedding_model( - ls_config, + ogx_config, provider_id=provider_id, embedding_model=embedding_model, embedding_dimension=embedding_dimension, @@ -763,7 +765,7 @@ def _enrich_one_vector_store_provider( def enrich_vector_store( - ls_config: dict[str, Any], + ogx_config: dict[str, Any], vector_store: Optional[dict[str, Any]] = None, ) -> None: """Enrich LS config with dynamic vector-store provider capacity. @@ -775,7 +777,7 @@ def enrich_vector_store( ``registered_resources.vector_stores``. Parameters: - ls_config: OGX configuration dictionary (modified in place). + ogx_config: OGX configuration dictionary (modified in place). vector_store: High-level ``vector_store`` section (``default_provider`` + ``providers``) as a dict. """ @@ -783,16 +785,16 @@ def enrich_vector_store( providers = vector_store.get("providers") or [] if not providers: logger.debug("vector_store.providers not configured: skipping") - dedupe_providers_vector_io(ls_config) + dedupe_providers_vector_io(ogx_config) return - backends = ls_config.setdefault("storage", {}).setdefault("backends", {}) - providers_section = ls_config.setdefault("providers", {}) + backends = ogx_config.setdefault("storage", {}).setdefault("backends", {}) + providers_section = ogx_config.setdefault("providers", {}) vector_io = providers_section.get("vector_io") if not isinstance(vector_io, list): vector_io = [] providers_section["vector_io"] = vector_io - ls_config.setdefault("registered_resources", {}).setdefault("models", []) + ogx_config.setdefault("registered_resources", {}).setdefault("models", []) existing_ids = { str(entry.get("provider_id")).strip() @@ -802,16 +804,16 @@ def enrich_vector_store( for entry in providers: _enrich_one_vector_store_provider( - entry, backends, vector_io, existing_ids, ls_config + entry, backends, vector_io, existing_ids, ogx_config ) designated = _vector_store_provider_by_id( providers, vector_store.get("default_provider") ) if designated is not None: - _apply_vector_stores_defaults(ls_config, designated) + _apply_vector_stores_defaults(ogx_config, designated) - dedupe_providers_vector_io(ls_config) + dedupe_providers_vector_io(ogx_config) # ============================================================================= @@ -820,14 +822,14 @@ def enrich_vector_store( def enrich_solr( # pylint: disable=too-many-locals,too-many-statements - ls_config: dict[str, Any], + ogx_config: dict[str, Any], rag_config: dict[str, Any], okp_config: dict[str, Any], ) -> None: """Enrich OGX config with Solr settings. Parameters: - ls_config: OGX configuration dict (modified in place) + ogx_config: OGX configuration dict (modified in place) rag_config: RAG configuration dict. Used keys: - inline (list[str]): inline RAG IDs - tool (list[str]): tool RAG IDs @@ -861,14 +863,14 @@ def enrich_solr( # pylint: disable=too-many-locals,too-many-statements logger.info("Enriching OGX config with OKP") # Add vector_io provider for Solr - if "providers" not in ls_config: - ls_config["providers"] = {} - if "vector_io" not in ls_config["providers"]: - ls_config["providers"]["vector_io"] = [] + if "providers" not in ogx_config: + ogx_config["providers"] = {} + if "vector_io" not in ogx_config["providers"]: + ogx_config["providers"]["vector_io"] = [] # Add Solr provider if not already present existing_providers = [ - p.get("provider_id") for p in ls_config["providers"]["vector_io"] + p.get("provider_id") for p in ogx_config["providers"]["vector_io"] ] if constants.SOLR_PROVIDER_ID not in existing_providers: collection_env = ( @@ -886,7 +888,7 @@ def enrich_solr( # pylint: disable=too-many-locals,too-many-statements embedding_dim_env = ( f"${{env.SOLR_EMBEDDING_DIM:={constants.SOLR_DEFAULT_EMBEDDING_DIMENSION}}}" ) - ls_config["providers"]["vector_io"].append( + ogx_config["providers"]["vector_io"].append( { "provider_id": constants.SOLR_PROVIDER_ID, "provider_type": "remote::solr_vector_io", @@ -919,18 +921,18 @@ def enrich_solr( # pylint: disable=too-many-locals,too-many-statements logger.info("Added OKP provider to providers/vector_io") # Add vector store registration for Solr - if "registered_resources" not in ls_config: - ls_config["registered_resources"] = {} - if "vector_stores" not in ls_config["registered_resources"]: - ls_config["registered_resources"]["vector_stores"] = [] + if "registered_resources" not in ogx_config: + ogx_config["registered_resources"] = {} + if "vector_stores" not in ogx_config["registered_resources"]: + ogx_config["registered_resources"]["vector_stores"] = [] # Add Solr vector store if not already present existing_stores = [ vs.get("vector_store_id") - for vs in ls_config["registered_resources"]["vector_stores"] + for vs in ogx_config["registered_resources"]["vector_stores"] ] if constants.SOLR_DEFAULT_VECTOR_STORE_ID not in existing_stores: - ls_config["registered_resources"]["vector_stores"].append( + ogx_config["registered_resources"]["vector_stores"].append( { "vector_store_id": constants.SOLR_DEFAULT_VECTOR_STORE_ID, "provider_id": constants.SOLR_PROVIDER_ID, @@ -944,21 +946,21 @@ def enrich_solr( # pylint: disable=too-many-locals,too-many-statements ) # Add Solr embedding model to registered_resources.models if not already present - if "models" not in ls_config["registered_resources"]: - ls_config["registered_resources"]["models"] = [] + if "models" not in ogx_config["registered_resources"]: + ogx_config["registered_resources"]["models"] = [] # Strip sentence-transformers/ prefix from constant for provider_model_id provider_model_id = constants.SOLR_DEFAULT_EMBEDDING_MODEL provider_model_id = provider_model_id.removeprefix("sentence-transformers/") # Check if already registered - registered_models = ls_config["registered_resources"]["models"] + registered_models = ogx_config["registered_resources"]["models"] existing_model_ids = [m.get("provider_model_id") for m in registered_models] if provider_model_id not in existing_model_ids: # Build environment variable expression provider_model_env = f"${{env.SOLR_EMBEDDING_MODEL:={provider_model_id}}}" - ls_config["registered_resources"]["models"].append( + ogx_config["registered_resources"]["models"].append( { "model_id": constants.SOLR_EMBEDDING_MODEL_ID, "model_type": "embedding", @@ -981,9 +983,9 @@ def enrich_solr( # pylint: disable=too-many-locals,too-many-statements # LCORE uses "semantic"; OGX uses "vector" if ogx_mode == "semantic": ogx_mode = "vector" - if "vector_stores" not in ls_config: - ls_config["vector_stores"] = {} - chunk_params = ls_config["vector_stores"].setdefault( + if "vector_stores" not in ogx_config: + ogx_config["vector_stores"] = {} + chunk_params = ogx_config["vector_stores"].setdefault( "chunk_retrieval_params", {} ) chunk_params["default_search_mode"] = ogx_mode @@ -1059,17 +1061,17 @@ def _matchable_provider_id(provider_id: Any) -> Any: return provider_id -def _strip_default_openai_inference(ls_config: dict[str, Any]) -> None: +def _strip_default_openai_inference(ogx_config: dict[str, Any]) -> None: """Remove the OpenAI inference provider from the default baseline. Parameters: - ls_config: The Llama Stack configuration being synthesized (modified + ogx_config: The OGX configuration being synthesized (modified in place). Returns: - None: ``ls_config`` is modified in place. + None: ``ogx_config`` is modified in place. """ - providers = ls_config.get("providers") + providers = ogx_config.get("providers") if not isinstance(providers, dict): return inference = providers.get("inference") @@ -1086,7 +1088,7 @@ def _strip_default_openai_inference(ls_config: dict[str, Any]) -> None: def apply_high_level_inference( - ls_config: dict[str, Any], inference: dict[str, Any] + ogx_config: dict[str, Any], inference: dict[str, Any] ) -> None: """Expand high-level ``inference.providers`` into OGX provider entries. @@ -1104,35 +1106,35 @@ def apply_high_level_inference( values (R6). Parameters: - ls_config: The OGX configuration being synthesized (modified in + ogx_config: The OGX configuration being synthesized (modified in place). inference: The root ``inference`` section as a dict; only its ``providers`` list is consumed here. Returns: - None: ``ls_config`` is modified in place. + None: ``ogx_config`` is modified in place. """ providers = inference.get("providers") or [] if not providers: return - providers_section = ls_config.setdefault("providers", {}) + providers_section = ogx_config.setdefault("providers", {}) inference_list = providers_section.setdefault("inference", []) for provider in providers: provider_type = provider["type"] emitted_id = provider.get("id") or provider_type.replace("_", "-") - ls_provider_type = PROVIDER_TYPE_MAP[provider_type] + ogx_provider_type = PROVIDER_TYPE_MAP[provider_type] entry: dict[str, Any] = { "provider_id": emitted_id, - "provider_type": ls_provider_type, + "provider_type": ogx_provider_type, } provider_config: dict[str, Any] = {} if provider.get("extra"): provider_config.update(provider["extra"]) if provider.get("api_key_env"): - key_field = API_KEY_FIELD_MAP.get(ls_provider_type, "api_key") + key_field = API_KEY_FIELD_MAP.get(ogx_provider_type, "api_key") provider_config[key_field] = "${env." + provider["api_key_env"] + "}" if provider.get("allowed_models"): provider_config["allowed_models"] = provider["allowed_models"] @@ -1162,8 +1164,8 @@ def apply_high_level_inference( ) -def ensure_mcp_tool_runtime(ls_config: dict[str, Any]) -> None: - """Ensure the default MCP tool_runtime provider exists in ``ls_config``. +def ensure_mcp_tool_runtime(ogx_config: dict[str, Any]) -> None: + """Ensure the default MCP tool_runtime provider exists in ``ogx_config``. Adds ``tool_runtime`` to ``apis`` when missing, then appends the default ``model-context-protocol`` provider under ``providers.tool_runtime`` when @@ -1171,17 +1173,17 @@ def ensure_mcp_tool_runtime(ls_config: dict[str, Any]) -> None: (including ``rag-runtime``) are left untouched. Parameters: - ls_config: The OGX configuration being synthesized (modified + ogx_config: The OGX configuration being synthesized (modified in place). Returns: - None: ``ls_config`` is modified in place. + None: ``ogx_config`` is modified in place. """ - apis = ls_config.setdefault("apis", []) + apis = ogx_config.setdefault("apis", []) if "tool_runtime" not in apis: apis.append("tool_runtime") - providers_section = ls_config.setdefault("providers", {}) + providers_section = ogx_config.setdefault("providers", {}) tool_runtime = providers_section.setdefault("tool_runtime", []) for existing in tool_runtime: if ( @@ -1272,7 +1274,7 @@ def synthesize_configuration( # pylint: disable=too-many-locals else load_default_baseline() ) - ls_config: dict[str, Any] = copy.deepcopy(baseline) + ogx_config: dict[str, Any] = copy.deepcopy(baseline) # Profile and empty are unchanged. The shipped file either keeps OpenAI # (default/omitted, with a deprecation WARN) or drops it (byo-llm). @@ -1283,7 +1285,7 @@ def synthesize_configuration( # pylint: disable=too-many-locals # Engineering Support Agreement's one-minor deprecation phase applies. if loaded_shipped_baseline: if unified and unified.get("baseline") == "byo-llm": - _strip_default_openai_inference(ls_config) + _strip_default_openai_inference(ogx_config) else: logger.warning( "DEPRECATED: the built-in OpenAI inference provider in " @@ -1296,23 +1298,23 @@ def synthesize_configuration( # pylint: disable=too-many-locals ) # 3. Normalize duplicated vector_io providers in the baseline. - dedupe_providers_vector_io(ls_config) + dedupe_providers_vector_io(ogx_config) # 4. High-level inference providers (Decision S5 — a root-level section). inference = lcs_config.get("inference") or {} if inference.get("providers"): - apply_high_level_inference(ls_config, inference) + apply_high_level_inference(ogx_config, inference) # 5. Ensure MCP tool_runtime for default/profile baselines (skipped for # baseline: empty so migrate round-trips stay lossless). if not baseline_was_empty: - ensure_mcp_tool_runtime(ls_config) + ensure_mcp_tool_runtime(ogx_config) # 6. Raw escape hatch, deep-merged with list replacement. It wins over the # baseline and the high-level expansion (R5) but deliberately NOT over # enrichment (step 7). if unified and unified.get("native_override"): - ls_config = deep_merge_list_replace(ls_config, unified["native_override"]) + ogx_config = deep_merge_list_replace(ogx_config, unified["native_override"]) # 7. Existing enrichment — same calls as legacy generate_configuration so # unified output matches legacy output for equivalent inputs (R7). @@ -1322,23 +1324,23 @@ def synthesize_configuration( # pylint: disable=too-many-locals # get the same treatment or list-shaped enrichment artifacts # (vector_io providers, registered models, azure model_validation) are # replaced wholesale by the lifted lists and silently lost. - enrich_azure_entra_id_inference(ls_config, lcs_config.get("azure_entra_id")) + enrich_azure_entra_id_inference(ogx_config, lcs_config.get("azure_entra_id")) rag_section = lcs_config.get("rag", {}) byok_stores = rag_section.get("byok", {}).get("stores", []) - enrich_byok_rag(ls_config, byok_stores) + enrich_byok_rag(ogx_config, byok_stores) retrieval = rag_section.get("retrieval", {}) rag_config_for_solr = { "inline": retrieval.get("inline", {}).get("sources", []), "tool": retrieval.get("tool", {}).get("sources", []), } okp_config = rag_section.get("okp", {}) - enrich_solr(ls_config, rag_config_for_solr, okp_config) - enrich_vector_store(ls_config, lcs_config.get("vector_store")) + enrich_solr(ogx_config, rag_config_for_solr, okp_config) + enrich_vector_store(ogx_config, lcs_config.get("vector_store")) # 8. Dedupe again in case native_override or enrichment reintroduced dupes. - dedupe_providers_vector_io(ls_config) + dedupe_providers_vector_io(ogx_config) - return ls_config + return ogx_config def synthesize_to_file( @@ -1364,7 +1366,7 @@ def synthesize_to_file( Returns: None. """ - ls_config = synthesize_configuration(lcs_config, config_file_dir, default_baseline) + ogx_config = synthesize_configuration(lcs_config, config_file_dir, default_baseline) path = Path(output_file) if path.parent != Path(""): @@ -1374,7 +1376,7 @@ def synthesize_to_file( # the write guarantees 0600 even when overwriting a pre-existing file. fd = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) with os.fdopen(fd, "w", encoding="utf-8") as file: - yaml.dump(ls_config, file, Dumper=YamlDumper, default_flow_style=False) + yaml.dump(ogx_config, file, Dumper=YamlDumper, default_flow_style=False) os.chmod(str(path), 0o600) logger.info("Wrote synthesized OGX configuration to %s (mode 0600)", path) @@ -1480,17 +1482,17 @@ def generate_configuration( logger.info("Reading OGX configuration from file %s", input_file) with open(input_file, encoding="utf-8") as file: - ls_config = yaml.safe_load(file) + ogx_config = yaml.safe_load(file) - dedupe_providers_vector_io(ls_config) + dedupe_providers_vector_io(ogx_config) # Enrichment: Azure Entra ID deferred auth - enrich_azure_entra_id_inference(ls_config, config.get("azure_entra_id")) + enrich_azure_entra_id_inference(ogx_config, config.get("azure_entra_id")) # Enrichment: BYOK RAG rag_section = config.get("rag", {}) byok_stores = rag_section.get("byok", {}).get("stores", []) - enrich_byok_rag(ls_config, byok_stores) + enrich_byok_rag(ogx_config, byok_stores) # Enrichment: Solr - enabled when "okp" appears in either inline or tool list retrieval = rag_section.get("retrieval", {}) @@ -1499,14 +1501,14 @@ def generate_configuration( "tool": retrieval.get("tool", {}).get("sources", []), } okp_config = rag_section.get("okp", {}) - enrich_solr(ls_config, rag_config_for_solr, okp_config) + enrich_solr(ogx_config, rag_config_for_solr, okp_config) - dedupe_providers_vector_io(ls_config) + dedupe_providers_vector_io(ogx_config) logger.info("Writing OGX configuration into file %s", output_file) with open(output_file, "w", encoding="utf-8") as file: - yaml.dump(ls_config, file, Dumper=YamlDumper, default_flow_style=False) + yaml.dump(ogx_config, file, Dumper=YamlDumper, default_flow_style=False) # ============================================================================= diff --git a/src/telemetry/configuration_snapshot.py b/src/telemetry/configuration_snapshot.py index e7584e8c5..66f0c73ce 100644 --- a/src/telemetry/configuration_snapshot.py +++ b/src/telemetry/configuration_snapshot.py @@ -647,7 +647,7 @@ def _extract_snapshot_fields( # ============================================================================= -def _extract_store_info(ls_config: dict[str, Any], store_name: str) -> dict[str, Any]: +def _extract_store_info(ogx_config: dict[str, Any], store_name: str) -> dict[str, Any]: """Extract store type and db_path from OGX storage configuration. Resolves the store → backend → type/db_path chain in the OGX @@ -655,14 +655,14 @@ def _extract_store_info(ls_config: dict[str, Any], store_name: str) -> dict[str, Parameters: ---------- - ls_config: The parsed OGX configuration dict. + ogx_config: The parsed OGX configuration dict. store_name: Name of the store to look up (e.g., "inference", "metadata"). Returns: ------- A dict with 'type' and 'db_path' keys, plus 'namespace' for metadata store. """ - store = get_nested_value(ls_config, f"storage.stores.{store_name}") + store = get_nested_value(ogx_config, f"storage.stores.{store_name}") if store is None or not isinstance(store, dict): return {"type": NOT_CONFIGURED, "db_path": NOT_CONFIGURED} @@ -670,7 +670,7 @@ def _extract_store_info(ls_config: dict[str, Any], store_name: str) -> dict[str, if backend_name is None: return {"type": NOT_CONFIGURED, "db_path": NOT_CONFIGURED} - backends = get_nested_value(ls_config, "storage.backends") or {} + backends = get_nested_value(ogx_config, "storage.backends") or {} backend = backends.get(backend_name, {}) result: dict[str, Any] = { @@ -750,15 +750,15 @@ async def build_ogx_snapshot( if config_path is None: return {"status": NOT_AVAILABLE} - ls_config = await asyncio.to_thread(_read_yaml_file, config_path) + ogx_config = await asyncio.to_thread(_read_yaml_file, config_path) - if not isinstance(ls_config, dict): + if not isinstance(ogx_config, dict): logger.warning("OGX config is not a dict, skipping snapshot") return {"status": NOT_AVAILABLE} - snapshot = _extract_snapshot_fields(ls_config, OGX_FIELDS) - snapshot["inference_store"] = _extract_store_info(ls_config, "inference") - snapshot["metadata_store"] = _extract_store_info(ls_config, "metadata") + snapshot = _extract_snapshot_fields(ogx_config, OGX_FIELDS) + snapshot["inference_store"] = _extract_store_info(ogx_config, "inference") + snapshot["metadata_store"] = _extract_store_info(ogx_config, "metadata") return snapshot diff --git a/src/utils/conversations.py b/src/utils/conversations.py index b52d417bb..373463b36 100644 --- a/src/utils/conversations.py +++ b/src/utils/conversations.py @@ -564,7 +564,7 @@ async def append_turn_items_to_conversation( async def get_all_conversation_items( client: AsyncOgxClient, - conversation_id_llama_stack: str, + conversation_id_ogx: str, ) -> list[ConversationItem]: """Fetch all items for a conversation (Conversations API), paginating as needed. @@ -581,7 +581,7 @@ async def get_all_conversation_items( try: while has_more: page = await client.items.list( - conversation_id=conversation_id_llama_stack, + conversation_id=conversation_id_ogx, order="asc", after=after, ) diff --git a/tests/e2e-prow/rhoai/manifests/lightspeed/lightspeed-stack.yaml b/tests/e2e-prow/rhoai/manifests/lightspeed/lightspeed-stack.yaml index 9d7d3a58a..7e3c244d3 100644 --- a/tests/e2e-prow/rhoai/manifests/lightspeed/lightspeed-stack.yaml +++ b/tests/e2e-prow/rhoai/manifests/lightspeed/lightspeed-stack.yaml @@ -43,7 +43,7 @@ spec: seccompProfile: type: RuntimeDefault env: - - name: E2E_LLAMA_HOSTNAME + - name: E2E_OGX_HOSTNAME valueFrom: secretKeyRef: name: llama-stack-ip-secret diff --git a/tests/e2e-prow/rhoai/manifests/lightspeed/ogx-openai.yaml b/tests/e2e-prow/rhoai/manifests/lightspeed/ogx-openai.yaml index 20ec60a8b..28e79a1dc 100644 --- a/tests/e2e-prow/rhoai/manifests/lightspeed/ogx-openai.yaml +++ b/tests/e2e-prow/rhoai/manifests/lightspeed/ogx-openai.yaml @@ -183,7 +183,7 @@ spec: secretKeyRef: name: faiss-vector-store-secret key: id - - name: E2E_LLAMA_HOSTNAME + - name: E2E_OGX_HOSTNAME valueFrom: secretKeyRef: name: llama-stack-ip-secret diff --git a/tests/e2e-prow/rhoai/manifests/lightspeed/ogx-prow.yaml b/tests/e2e-prow/rhoai/manifests/lightspeed/ogx-prow.yaml index 2efea832c..22fa0caf2 100644 --- a/tests/e2e-prow/rhoai/manifests/lightspeed/ogx-prow.yaml +++ b/tests/e2e-prow/rhoai/manifests/lightspeed/ogx-prow.yaml @@ -126,7 +126,7 @@ spec: secretKeyRef: name: faiss-vector-store-secret key: id - - name: E2E_LLAMA_HOSTNAME + - name: E2E_OGX_HOSTNAME valueFrom: secretKeyRef: name: llama-stack-ip-secret @@ -155,7 +155,7 @@ spec: ENRICHED_CONFIG="/opt/app-root/run.yaml" LIGHTSPEED_CONFIG="${LIGHTSPEED_CONFIG:-/opt/app-root/lightspeed-stack.yaml}" if [[ -f "$LIGHTSPEED_CONFIG" ]]; then - echo "Enriching llama-stack config..." + echo "Enriching OGX config..." ENRICHMENT_FAILED=0 /opt/app-root/.venv/bin/python3 /opt/app-root/src/ogx_configuration.py \ -c "$LIGHTSPEED_CONFIG" \ diff --git a/tests/e2e-prow/rhoai/pipeline-konflux.sh b/tests/e2e-prow/rhoai/pipeline-konflux.sh index 14020dbcf..5697dbea2 100755 --- a/tests/e2e-prow/rhoai/pipeline-konflux.sh +++ b/tests/e2e-prow/rhoai/pipeline-konflux.sh @@ -296,9 +296,9 @@ fi # So behave/e2e-ops can kill this listener before rebinding 8080 (restart-lightspeed hooks). # Debug hook/port churn: export E2E_OPS_VERBOSE=1 before running pipeline.sh export E2E_LSC_PORT_FORWARD_PID_FILE="${E2E_LSC_PORT_FORWARD_PID_FILE:-/tmp/e2e-lightspeed-port-forward.pid}" -export E2E_LLAMA_PORT_FORWARD_PID_FILE="${E2E_LLAMA_PORT_FORWARD_PID_FILE:-/tmp/e2e-llama-port-forward.pid}" +export E2E_OGX_PORT_FORWARD_PID_FILE="${E2E_OGX_PORT_FORWARD_PID_FILE:-/tmp/e2e-ogx-port-forward.pid}" rm -f "$E2E_LSC_PORT_FORWARD_PID_FILE" -rm -f "$E2E_LLAMA_PORT_FORWARD_PID_FILE" +rm -f "$E2E_OGX_PORT_FORWARD_PID_FILE" oc label pod lightspeed-stack-service pod=lightspeed-stack-service -n $NAMESPACE @@ -336,8 +336,8 @@ PF_JWKS_PID=$! # OGX directly — mirror LCS and forward llama-stack-service-svc to localhost:8321. log "Starting port-forward for llama-stack (MCP / ogx_client hooks)..." oc port-forward svc/llama-stack-service-svc 8321:8321 -n $NAMESPACE & -PF_LLAMA_PID=$! -echo "$PF_LLAMA_PID" >"$E2E_LLAMA_PORT_FORWARD_PID_FILE" +PF_OGX_PID=$! +echo "$PF_OGX_PID" >"$E2E_OGX_PORT_FORWARD_PID_FILE" # Wait for port-forward to be usable (app may not be listening immediately; port-forward can drop) log "Waiting for port-forward to lightspeed-stack to be ready..." @@ -358,7 +358,7 @@ for i in $(seq 1 36); do done < <(oc get events -n "$NAMESPACE" --sort-by='.lastTimestamp' 2>&1 | tail -40) || true kill $PF_LCS_PID 2>/dev/null || true kill $PF_JWKS_PID 2>/dev/null || true - kill $PF_LLAMA_PID 2>/dev/null || true + kill $PF_OGX_PID 2>/dev/null || true exit 1 fi # If port-forward process died, restart it (e.g. "connection refused" / "lost connection to pod") @@ -384,22 +384,22 @@ for i in $(seq 1 36); do e2e_echo_pod_logs 250 kill $PF_LCS_PID 2>/dev/null || true kill $PF_JWKS_PID 2>/dev/null || true - kill $PF_LLAMA_PID 2>/dev/null || true + kill $PF_OGX_PID 2>/dev/null || true exit 1 fi - if ! kill -0 $PF_LLAMA_PID 2>/dev/null; then + if ! kill -0 $PF_OGX_PID 2>/dev/null; then log "Llama port-forward died, restarting (attempt $i)..." oc port-forward svc/llama-stack-service-svc 8321:8321 -n $NAMESPACE & - PF_LLAMA_PID=$! - echo "$PF_LLAMA_PID" >"$E2E_LLAMA_PORT_FORWARD_PID_FILE" + PF_OGX_PID=$! + echo "$PF_OGX_PID" >"$E2E_OGX_PORT_FORWARD_PID_FILE" fi sleep 5 done export E2E_LSC_HOSTNAME="localhost" export E2E_JWKS_HOSTNAME="localhost" -export E2E_LLAMA_HOSTNAME="localhost" -export E2E_LLAMA_PORT="8321" +export E2E_OGX_HOSTNAME="localhost" +export E2E_OGX_PORT="8321" # Same pattern as tests/e2e-prow/rhoai/pipeline.sh and .github/workflows/e2e_tests_*.yaml: # Behave {MODEL}/{PROVIDER} use these when set; avoids wrong fallbacks if /v1/models # discovery in before_all is empty (matches run-ci.yaml openai + E2E_OPENAI_MODEL). @@ -413,7 +413,7 @@ fi export E2E_DEFAULT_PROVIDER_OVERRIDE E2E_DEFAULT_MODEL_OVERRIDE log "LCS accessible at: http://$E2E_LSC_HOSTNAME:8080" log "Mock JWKS accessible at: http://$E2E_JWKS_HOSTNAME:8000" -log "OGX (e2e client hooks) at: http://$E2E_LLAMA_HOSTNAME:$E2E_LLAMA_PORT" +log "OGX (e2e client hooks) at: http://$E2E_OGX_HOSTNAME:$E2E_OGX_PORT" #======================================== # 7. RUN TESTS @@ -441,20 +441,20 @@ if [[ -n "${E2E_LSC_PORT_FORWARD_PID_FILE:-}" && -f "$E2E_LSC_PORT_FORWARD_PID_F fi rm -f "$E2E_LSC_PORT_FORWARD_PID_FILE" fi -if [[ -n "${E2E_LLAMA_PORT_FORWARD_PID_FILE:-}" && -f "$E2E_LLAMA_PORT_FORWARD_PID_FILE" ]]; then - read -r _ll_pf <"$E2E_LLAMA_PORT_FORWARD_PID_FILE" 2>/dev/null || true +if [[ -n "${E2E_OGX_PORT_FORWARD_PID_FILE:-}" && -f "$E2E_OGX_PORT_FORWARD_PID_FILE" ]]; then + read -r _ll_pf <"$E2E_OGX_PORT_FORWARD_PID_FILE" 2>/dev/null || true if [[ "${_ll_pf:-}" =~ ^[0-9]+$ ]]; then kill -9 "$_ll_pf" 2>/dev/null || true fi - rm -f "$E2E_LLAMA_PORT_FORWARD_PID_FILE" + rm -f "$E2E_OGX_PORT_FORWARD_PID_FILE" fi kill $PF_LCS_PID 2>/dev/null || true kill $PF_JWKS_PID 2>/dev/null || true -kill $PF_LLAMA_PID 2>/dev/null || true +kill $PF_OGX_PID 2>/dev/null || true wait $PF_LCS_PID 2>/dev/null || true wait $PF_JWKS_PID 2>/dev/null || true -wait $PF_LLAMA_PID 2>/dev/null || true +wait $PF_OGX_PID 2>/dev/null || true set -e trap 'echo "❌ Pipeline failed at line $LINENO"; exit 1' ERR diff --git a/tests/e2e-prow/rhoai/pipeline-services-konflux.sh b/tests/e2e-prow/rhoai/pipeline-services-konflux.sh index b34a77959..d907c1fce 100755 --- a/tests/e2e-prow/rhoai/pipeline-services-konflux.sh +++ b/tests/e2e-prow/rhoai/pipeline-services-konflux.sh @@ -19,7 +19,7 @@ if [ -f "$REPO_ROOT/tests/e2e/secrets/invalid-mcp-token" ]; then fi # 1. OGX (run from source). Cluster DNS name matches oc expose --name=llama-stack-service-svc. -# Secret must exist before the pod: both LCS and OGX-container use E2E_LLAMA_HOSTNAME from it. +# Secret must exist before the pod: both LCS and OGX-container use E2E_OGX_HOSTNAME from it. _LLAMA_SVC_FQDN="llama-stack-service-svc.${NAMESPACE}.svc.cluster.local" oc create secret generic llama-stack-ip-secret \ --from-literal=key="$_LLAMA_SVC_FQDN" \ diff --git a/tests/e2e-prow/rhoai/pipeline-services.sh b/tests/e2e-prow/rhoai/pipeline-services.sh index 152e54392..29badff28 100755 --- a/tests/e2e-prow/rhoai/pipeline-services.sh +++ b/tests/e2e-prow/rhoai/pipeline-services.sh @@ -4,9 +4,9 @@ BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" NAMESPACE="${NAMESPACE:-e2e-rhoai-dsc}" # Create OGX-ip-secret before deploying the pod (it references the secret as an env var) -export E2E_LLAMA_HOSTNAME="llama-stack-service-svc.${NAMESPACE}.svc.cluster.local" +export E2E_OGX_HOSTNAME="llama-stack-service-svc.${NAMESPACE}.svc.cluster.local" oc create secret generic llama-stack-ip-secret \ - --from-literal=key="$E2E_LLAMA_HOSTNAME" \ + --from-literal=key="$E2E_OGX_HOSTNAME" \ -n "$NAMESPACE" 2>/dev/null || echo "Secret llama-stack-ip-secret exists" # Deploy OGX (substitute only LLAMA_STACK_IMAGE, leave other ${} intact) diff --git a/tests/e2e-prow/rhoai/pipeline.sh b/tests/e2e-prow/rhoai/pipeline.sh index fab42c8ae..af0ebf592 100755 --- a/tests/e2e-prow/rhoai/pipeline.sh +++ b/tests/e2e-prow/rhoai/pipeline.sh @@ -356,10 +356,10 @@ oc describe pod llama-stack-service -n "$NAMESPACE" || true # Export PID file paths so e2e-ops.sh can find and kill stale port-forwards # during test-triggered pod restarts (matches pipeline-konflux.sh). export E2E_LSC_PORT_FORWARD_PID_FILE="${E2E_LSC_PORT_FORWARD_PID_FILE:-/tmp/e2e-lightspeed-port-forward.pid}" -export E2E_LLAMA_PORT_FORWARD_PID_FILE="${E2E_LLAMA_PORT_FORWARD_PID_FILE:-/tmp/e2e-llama-port-forward.pid}" +export E2E_OGX_PORT_FORWARD_PID_FILE="${E2E_OGX_PORT_FORWARD_PID_FILE:-/tmp/e2e-ogx-port-forward.pid}" export E2E_JWKS_PORT_FORWARD_PID_FILE="${E2E_JWKS_PORT_FORWARD_PID_FILE:-/tmp/e2e-jwks-port-forward.pid}" rm -f "$E2E_LSC_PORT_FORWARD_PID_FILE" -rm -f "$E2E_LLAMA_PORT_FORWARD_PID_FILE" +rm -f "$E2E_OGX_PORT_FORWARD_PID_FILE" rm -f "$E2E_JWKS_PORT_FORWARD_PID_FILE" oc label pod lightspeed-stack-service pod=lightspeed-stack-service -n $NAMESPACE @@ -398,8 +398,8 @@ echo "$PF_JWKS_PID" >"$E2E_JWKS_PORT_FORWARD_PID_FILE" # need localhost:8321. Without this forward those tests hit "Connection refused". echo "Starting port-forward for llama-stack..." oc port-forward svc/llama-stack-service-svc 8321:8321 -n $NAMESPACE & -PF_LLAMA_PID=$! -echo "$PF_LLAMA_PID" >"$E2E_LLAMA_PORT_FORWARD_PID_FILE" +PF_OGX_PID=$! +echo "$PF_OGX_PID" >"$E2E_OGX_PORT_FORWARD_PID_FILE" # Wait for port-forward to be usable (app may not be listening immediately; port-forward can drop) echo "Waiting for port-forward to lightspeed-stack to be ready..." @@ -421,7 +421,7 @@ for i in $(seq 1 36); do oc get pods -n "$NAMESPACE" -o wide || true kill $PF_LCS_PID 2>/dev/null || true kill $PF_JWKS_PID 2>/dev/null || true - kill $PF_LLAMA_PID 2>/dev/null || true + kill $PF_OGX_PID 2>/dev/null || true exit 1 fi # If port-forward process died, restart it (e.g. "connection refused" / "lost connection to pod") @@ -446,14 +446,14 @@ for i in $(seq 1 36); do oc logs llama-stack-service -n "$NAMESPACE" --tail=100 || true kill $PF_LCS_PID 2>/dev/null || true kill $PF_JWKS_PID 2>/dev/null || true - kill $PF_LLAMA_PID 2>/dev/null || true + kill $PF_OGX_PID 2>/dev/null || true exit 1 fi - if ! kill -0 $PF_LLAMA_PID 2>/dev/null; then + if ! kill -0 $PF_OGX_PID 2>/dev/null; then echo "Llama port-forward died, restarting (attempt $i)..." oc port-forward svc/llama-stack-service-svc 8321:8321 -n $NAMESPACE & - PF_LLAMA_PID=$! - echo "$PF_LLAMA_PID" >"$E2E_LLAMA_PORT_FORWARD_PID_FILE" + PF_OGX_PID=$! + echo "$PF_OGX_PID" >"$E2E_OGX_PORT_FORWARD_PID_FILE" fi sleep 5 done @@ -487,11 +487,11 @@ TEST_EXIT_CODE=$(cat "$E2E_EXIT_CODE_FILE" 2>/dev/null || echo 1) # Kill first so wait doesn't block (if a port-forward is still running, wait would hang) kill $PF_LCS_PID 2>/dev/null || true kill $PF_JWKS_PID 2>/dev/null || true -kill $PF_LLAMA_PID 2>/dev/null || true +kill $PF_OGX_PID 2>/dev/null || true wait $PF_LCS_PID 2>/dev/null || true wait $PF_JWKS_PID 2>/dev/null || true -wait $PF_LLAMA_PID 2>/dev/null || true -rm -f "$E2E_LSC_PORT_FORWARD_PID_FILE" "$E2E_LLAMA_PORT_FORWARD_PID_FILE" "$E2E_JWKS_PORT_FORWARD_PID_FILE" +wait $PF_OGX_PID 2>/dev/null || true +rm -f "$E2E_LSC_PORT_FORWARD_PID_FILE" "$E2E_OGX_PORT_FORWARD_PID_FILE" "$E2E_JWKS_PORT_FORWARD_PID_FILE" set -e trap 'echo "❌ Pipeline failed at line $LINENO"; exit 1' ERR diff --git a/tests/e2e-prow/rhoai/scripts/e2e-ops.sh b/tests/e2e-prow/rhoai/scripts/e2e-ops.sh index 955d8463d..d3dd02b57 100755 --- a/tests/e2e-prow/rhoai/scripts/e2e-ops.sh +++ b/tests/e2e-prow/rhoai/scripts/e2e-ops.sh @@ -12,21 +12,21 @@ # Behave steps that call OGX directly (MCP toolgroups, shields). When the llama # pod is recreated, that forward must be restarted or you get "PodSandbox ... not found" / # APIConnectionError on subsequent scenarios. -# - E2E_LLAMA_PORT_FORWARD_PID_FILE coordinates killing/restarting the 8321 forward. +# - E2E_OGX_PORT_FORWARD_PID_FILE coordinates killing/restarting the 8321 forward. # - restart-lightspeed ensures Llama is running before LCS recreate when needed. -# - restart-both-services is available explicitly; restart-lightspeed / restart-llama-stack +# - restart-both-services is available explicitly; restart-lightspeed / restart-ogx # do not auto-trigger a full stack restart on failure. # # Commands: # restart-lightspeed - Restart lightspeed-stack pod and port-forward -# restart-llama-stack - Restart/restore OGX pod and localhost:8321 forward +# restart-ogx - Restart/restore OGX pod and localhost:8321 forward # restart-both-services - Full OGX then lightspeed-stack restart (explicit only) # restart-port-forward - Re-establish port-forward for lightspeed -# restart-llama-port-forward - Re-establish port-forward for OGX (8321) +# restart-ogx-port-forward - Re-establish port-forward for OGX (8321) # wait-for-pod [attempts] - Wait for a pod to be ready # update-configmap - Update ConfigMap from file # get-configmap-content - Get ConfigMap content (outputs to stdout) -# disrupt-llama-stack - Delete OGX pod to disrupt connection +# disrupt-ogx - Delete OGX pod to disrupt connection # deploy-e2e-tunnel-proxy - Deploy in-cluster tunnel proxy (proxy.feature step) # deploy-e2e-interception-proxy - Deploy in-cluster interception proxy (proxy.feature step) # deploy-e2e-mock-tls-inference - Deploy mock HTTPS inference server (tls-*.feature) @@ -41,7 +41,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" MANIFEST_DIR="$SCRIPT_DIR/../manifests/lightspeed" # Written by pipeline.sh when it starts LCS port-forward; e2e-ops kills this PID before rebinding 8080. E2E_LSC_PORT_FORWARD_PID_FILE="${E2E_LSC_PORT_FORWARD_PID_FILE:-/tmp/e2e-lightspeed-port-forward.pid}" -E2E_LLAMA_PORT_FORWARD_PID_FILE="${E2E_LLAMA_PORT_FORWARD_PID_FILE:-/tmp/e2e-llama-port-forward.pid}" +E2E_OGX_PORT_FORWARD_PID_FILE="${E2E_OGX_PORT_FORWARD_PID_FILE:-/tmp/e2e-ogx-port-forward.pid}" E2E_JWKS_PORT_FORWARD_PID_FILE="${E2E_JWKS_PORT_FORWARD_PID_FILE:-/tmp/e2e-jwks-port-forward.pid}" # ============================================================================ @@ -177,8 +177,8 @@ kill_stale_lightspeed_forward() { kill_stale_llama_forward() { local port="${1:-8321}" local saved_pf - if [[ -f "$E2E_LLAMA_PORT_FORWARD_PID_FILE" ]]; then - read -r saved_pf <"$E2E_LLAMA_PORT_FORWARD_PID_FILE" 2>/dev/null || true + if [[ -f "$E2E_OGX_PORT_FORWARD_PID_FILE" ]]; then + read -r saved_pf <"$E2E_OGX_PORT_FORWARD_PID_FILE" 2>/dev/null || true if [[ "$saved_pf" =~ ^[0-9]+$ ]]; then kill -9 "$saved_pf" 2>/dev/null || true fi @@ -225,7 +225,7 @@ e2e_ops_diagnose_forward_failure() { tail -30 /tmp/port-forward.log 2>/dev/null | sed 's/^/[e2e-ops] /' || true fi e2e_ops_dump_pod_logs "lightspeed-stack-service" 10000 - if [[ "${E2E_COPY_MOCK_TLS_CERTS_TO_LLAMA:-0}" == "1" ]]; then + if [[ "${E2E_COPY_MOCK_TLS_CERTS_TO_OGX:-0}" == "1" ]]; then e2e_ops_dump_pod_logs "llama-stack-service" 10000 fi } @@ -333,14 +333,14 @@ _restart_llama_stack_core() { echo "Applying pod manifest..." if [[ "${E2E_KONFLUX_E2E:-0}" == "1" ]]; then - if [[ "${E2E_COPY_INTERCEPTION_CA_TO_LLAMA:-0}" == "1" ]]; then + if [[ "${E2E_COPY_INTERCEPTION_CA_TO_OGX:-0}" == "1" ]]; then echo "[e2e-ops] Syncing e2e-interception-proxy-ca secret before llama-stack apply..." if ! cmd_sync_interception_proxy_ca_secret; then echo "===== Llama-stack restore FAILED (interception CA secret sync) =====" return 1 fi fi - if [[ "${E2E_COPY_MOCK_TLS_CERTS_TO_LLAMA:-0}" == "1" ]] \ + if [[ "${E2E_COPY_MOCK_TLS_CERTS_TO_OGX:-0}" == "1" ]] \ && [[ "${E2E_SYNC_MOCK_TLS_CERTS:-0}" == "1" ]]; then echo "[e2e-ops] Syncing e2e-mock-tls-certs secret before llama-stack apply..." if ! cmd_sync_mock_tls_certs_secret; then @@ -365,7 +365,7 @@ _restart_llama_stack_core() { fi echo "Labeling pod for service..." oc label pod llama-stack-service pod=llama-stack-service -n "$NAMESPACE" --overwrite - if [[ "${E2E_COPY_INTERCEPTION_CA_TO_LLAMA:-0}" == "1" ]]; then + if [[ "${E2E_COPY_INTERCEPTION_CA_TO_OGX:-0}" == "1" ]]; then if ! _verify_interception_ca_mounted_in_llama; then echo "===== Llama-stack restore FAILED (interception CA not mounted) =====" return 1 @@ -387,7 +387,7 @@ _restart_llama_stack_core() { fi if ! cmd_restart_llama_port_forward; then - echo "ERROR: Llama pod is up but localhost:${LOCAL_LLAMA_PORT:-8321} port-forward failed" + echo "ERROR: Llama pod is up but localhost:${LOCAL_OGX_PORT:-8321} port-forward failed" e2e_ops_dump_pod_logs "llama-stack-service" 200 return 1 fi @@ -401,10 +401,10 @@ _restart_lightspeed_core() { echo "Restarting lightspeed-stack service..." # Degraded-mode e2e must start LCS while llama is down. Default path restores - # llama first so pods can come up; set E2E_SKIP_LLAMA_RESTORE_ON_LCS_RESTART=1 + # llama first so pods can come up; set E2E_SKIP_OGX_RESTORE_ON_LCS_RESTART=1 # to keep llama disrupted for allow_degraded_mode startup checks. - if [[ "${E2E_SKIP_LLAMA_RESTORE_ON_LCS_RESTART:-0}" == "1" ]]; then - echo "⚠️ Skipping llama restore before LCS restart (E2E_SKIP_LLAMA_RESTORE_ON_LCS_RESTART=1)" + if [[ "${E2E_SKIP_OGX_RESTORE_ON_LCS_RESTART:-0}" == "1" ]]; then + echo "⚠️ Skipping llama restore before LCS restart (E2E_SKIP_OGX_RESTORE_ON_LCS_RESTART=1)" elif ! _llama_stack_http_health_once 2>/dev/null; then echo "⚠️ OGX not healthy — restoring before LCS restart..." if ! _restart_llama_stack_core; then @@ -555,8 +555,8 @@ verify_llama_local_forward() { } cmd_restart_llama_port_forward() { - local local_port="${LOCAL_LLAMA_PORT:-8321}" - local remote_port="${REMOTE_LLAMA_PORT:-8321}" + local local_port="${LOCAL_OGX_PORT:-8321}" + local remote_port="${REMOTE_OGX_PORT:-8321}" local max_attempts=6 local pf_pid local pf_resource @@ -594,7 +594,7 @@ cmd_restart_llama_port_forward() { sleep 4 if verify_llama_local_forward 12; then - echo "$pf_pid" >"$E2E_LLAMA_PORT_FORWARD_PID_FILE" + echo "$pf_pid" >"$E2E_OGX_PORT_FORWARD_PID_FILE" echo "[e2e-ops] Llama through port-forward: GET http://127.0.0.1:$local_port/v1/health -> OK" echo "✓ OGX port-forward established (PID: $pf_pid, $pf_resource)" return 0 @@ -972,7 +972,7 @@ cmd_dump_pod_logs() { e2e_ops_dump_pod_logs "${1:?pod name required}" "${2:-150}" } -cmd_disrupt_llama_stack() { +cmd_disrupt_ogx() { local pod_name="llama-stack-service" local phase @@ -1000,13 +1000,13 @@ case "$COMMAND" in restart-lightspeed) cmd_restart_lightspeed ;; - restart-llama-stack) + restart-ogx) cmd_restart_llama_stack ;; restart-both-services) cmd_restart_both_services ;; - restart-llama-port-forward) + restart-ogx-port-forward) cmd_restart_llama_port_forward ;; restart-jwks-port-forward) @@ -1024,8 +1024,8 @@ case "$COMMAND" in get-configmap-content) cmd_get_configmap_content "$@" ;; - disrupt-llama-stack) - cmd_disrupt_llama_stack + disrupt-ogx) + cmd_disrupt_ogx ;; tunnel-proxy-stats) cmd_tunnel_proxy_stats @@ -1065,14 +1065,14 @@ case "$COMMAND" in echo "" echo "Commands:" echo " restart-lightspeed - Restart lightspeed-stack pod and port-forward" - echo " restart-llama-stack - Restart/restore llama-stack pod" + echo " restart-ogx - Restart/restore llama-stack pod" echo " restart-both-services - Full llama-stack + lightspeed-stack restart (explicit)" - echo " restart-llama-port-forward - Re-establish port-forward for OGX (8321)" + echo " restart-ogx-port-forward - Re-establish port-forward for OGX (8321)" echo " restart-port-forward - Re-establish port-forward for lightspeed" echo " wait-for-pod [attempts] - Wait for a pod to be ready" echo " update-configmap - Update ConfigMap from file" echo " get-configmap-content - Get ConfigMap content (outputs to stdout)" - echo " disrupt-llama-stack - Delete llama-stack pod to disrupt connection" + echo " disrupt-ogx - Delete llama-stack pod to disrupt connection" echo " tunnel-proxy-stats - JSON stats from in-cluster e2e-tunnel-proxy" echo " interception-proxy-stats - JSON stats from in-cluster e2e-interception-proxy" echo " copy-interception-proxy-ca-to-llama - Alias for sync-interception-proxy-ca-secret" diff --git a/tests/e2e/configuration/library-mode/lightspeed-stack-shields-empty.yaml b/tests/e2e/configuration/library-mode/lightspeed-stack-shields-empty.yaml index 0ccb9b2d5..17291966d 100644 --- a/tests/e2e/configuration/library-mode/lightspeed-stack-shields-empty.yaml +++ b/tests/e2e/configuration/library-mode/lightspeed-stack-shields-empty.yaml @@ -10,7 +10,7 @@ service: color_log: true access_log: true llama_stack: - # Library mode - embeds llama-stack as library + # Library mode - embeds OGX as library use_as_library_client: true # Unified mode: run.yaml (materialized per provider by CI/the harness) # is consumed as the synthesis profile instead of the legacy two-file path. diff --git a/tests/e2e/configuration/library-mode/lightspeed-stack-shields-override-disabled.yaml b/tests/e2e/configuration/library-mode/lightspeed-stack-shields-override-disabled.yaml index d93431032..442b55ca2 100644 --- a/tests/e2e/configuration/library-mode/lightspeed-stack-shields-override-disabled.yaml +++ b/tests/e2e/configuration/library-mode/lightspeed-stack-shields-override-disabled.yaml @@ -12,7 +12,7 @@ service: color_log: true access_log: true llama_stack: - # Library mode - embeds llama-stack as library + # Library mode - embeds OGX as library use_as_library_client: true # Unified mode: run.yaml (materialized per provider by CI/the harness) # is consumed as the synthesis profile instead of the legacy two-file path. diff --git a/tests/e2e/configuration/library-mode/lightspeed-stack-shields.yaml b/tests/e2e/configuration/library-mode/lightspeed-stack-shields.yaml index 715f254c6..b00d2250e 100644 --- a/tests/e2e/configuration/library-mode/lightspeed-stack-shields.yaml +++ b/tests/e2e/configuration/library-mode/lightspeed-stack-shields.yaml @@ -12,7 +12,7 @@ service: color_log: true access_log: true llama_stack: - # Library mode - embeds llama-stack as library + # Library mode - embeds OGX as library use_as_library_client: true # Unified mode: run.yaml (materialized per provider by CI/the harness) # is consumed as the synthesis profile instead of the legacy two-file path. diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-authorized.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-authorized.yaml index b39110c29..946e606f6 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-authorized.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-authorized.yaml @@ -17,7 +17,7 @@ ogx: # Alternative for "as library use" # use_as_library_client: true # library_client_config_path: - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-default.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-default.yaml index 12ff8fcca..294603b16 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-default.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-default.yaml @@ -13,7 +13,7 @@ service: access_log: true ogx: use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-degraded.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-degraded.yaml index 11d276e5d..798d407c1 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-degraded.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-degraded.yaml @@ -11,7 +11,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy # Enable degraded mode to allow startup without OGX allow_degraded_mode: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-api-auth.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-api-auth.yaml index 373717732..a246a7aa7 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-api-auth.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-api-auth.yaml @@ -11,7 +11,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-client-auth.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-client-auth.yaml index 25cbdf836..bedfbbe46 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-client-auth.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-client-auth.yaml @@ -9,7 +9,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-file-auth.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-file-auth.yaml index ca60006c9..3191da2ce 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-file-auth.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-file-auth.yaml @@ -9,7 +9,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-invalid.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-invalid.yaml index 976a51853..865336d2b 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-invalid.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-invalid.yaml @@ -12,7 +12,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-kubernetes-auth.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-kubernetes-auth.yaml index 39b61ef3a..8dadb72d8 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-kubernetes-auth.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-kubernetes-auth.yaml @@ -9,7 +9,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-oauth-auth.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-oauth-auth.yaml index 9237b6019..4c18a8ace 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-oauth-auth.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-oauth-auth.yaml @@ -9,7 +9,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp.yaml index 837d44aab..46e07d4b5 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp.yaml @@ -15,7 +15,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-negative.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-negative.yaml index d7a22b5ee..20a6c6f78 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-negative.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-negative.yaml @@ -18,7 +18,7 @@ ogx: # Alternative for "as library use" # use_as_library_client: true # library_client_config_path: - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-rbac.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-rbac.yaml index 237d80849..c17233a65 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-rbac.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-rbac.yaml @@ -11,7 +11,7 @@ service: ogx: use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-rh-identity.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-rh-identity.yaml index f6959cd97..a73efbc62 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-rh-identity.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-rh-identity.yaml @@ -10,7 +10,7 @@ service: access_log: true ogx: use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-rhelai.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-rhelai.yaml index c1294a5b0..ff0615804 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-rhelai.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-rhelai.yaml @@ -9,7 +9,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-rhoai.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-rhoai.yaml index c1294a5b0..ff0615804 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-rhoai.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-rhoai.yaml @@ -9,7 +9,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-shields-empty.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-shields-empty.yaml index 5df05d93a..02eb06cea 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-shields-empty.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-shields-empty.yaml @@ -10,9 +10,9 @@ service: color_log: true access_log: true llama_stack: - # Server mode - connects to separate llama-stack service + # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-shields-override-disabled.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-shields-override-disabled.yaml index b51d86570..645117283 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-shields-override-disabled.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-shields-override-disabled.yaml @@ -12,9 +12,9 @@ service: color_log: true access_log: true llama_stack: - # Server mode - connects to separate llama-stack service + # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-shields.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-shields.yaml index 17e011c0a..2ea4827db 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-shields.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-shields.yaml @@ -12,9 +12,9 @@ service: color_log: true access_log: true llama_stack: - # Server mode - connects to separate llama-stack service + # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-skills-directory.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-skills-directory.yaml index 02da39f40..6418577ce 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-skills-directory.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-skills-directory.yaml @@ -10,7 +10,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-skills.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-skills.yaml index 93f100ebe..3c7a1b7c4 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-skills.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-skills.yaml @@ -10,7 +10,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-tls.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-tls.yaml index 24a0817b8..8f78c833e 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-tls.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-tls.yaml @@ -10,7 +10,7 @@ service: access_log: true ogx: use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack.yaml index 680552574..ece1c9c66 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack.yaml @@ -9,7 +9,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/features/degraded_mode_startup.feature b/tests/e2e/features/degraded_mode_startup.feature index 27b5cc6cb..c8dd2038d 100644 --- a/tests/e2e/features/degraded_mode_startup.feature +++ b/tests/e2e/features/degraded_mode_startup.feature @@ -1,7 +1,7 @@ @cfg_degraded @skip-in-library-mode @Authorized Feature: Degraded mode startup - End-to-end scenarios that test LCORE startup behavior when llama-stack + End-to-end scenarios that test LCORE startup behavior when ogx is NOT available at startup time and allow_degraded_mode is enabled. These tests verify that LCORE metrics correctly reflect startup state @@ -13,26 +13,26 @@ Feature: Degraded mode startup And REST API service prefix is /v1 And the Lightspeed stack configuration directory is "tests/e2e/configuration" - Scenario: Degraded mode metric is set to 0.0 when started with llama-stack + Scenario: Degraded mode metric is set to 0.0 when started with ogx Given The service uses the lightspeed-stack-degraded.yaml configuration And The service is restarted When I access endpoint "metrics" using HTTP GET method Then The status code of the response is 200 And The response body contains "ls_started_in_degraded_mode 0.0" - Scenario: Degraded mode metric is set to 1.0 when started without llama-stack - Given The llama-stack connection is disrupted + Scenario: Degraded mode metric is set to 1.0 when started without ogx + Given The ogx connection is disrupted And The service uses the lightspeed-stack-degraded.yaml configuration - # Konflux restart-lightspeed otherwise restores llama before LCS boots. - And The service is restarted without restoring llama-stack + # Konflux restart-lightspeed otherwise restores OGX before LCS boots. + And The service is restarted without restoring ogx When I access endpoint "metrics" using HTTP GET method Then The status code of the response is 200 And The response body contains "ls_started_in_degraded_mode 1.0" - Scenario: Readiness endpoint reports degraded state when started without llama-stack - Given The llama-stack connection is disrupted + Scenario: Readiness endpoint reports degraded state when started without ogx + Given The ogx connection is disrupted And The service uses the lightspeed-stack-degraded.yaml configuration - And The service is restarted without restoring llama-stack + And The service is restarted without restoring ogx When I access endpoint "readiness" using HTTP GET method Then The status code of the response is 200 And The body of the response, ignoring the "providers" field, is the following diff --git a/tests/e2e/features/environment.py b/tests/e2e/features/environment.py index 4e449f86b..4fc2346fc 100644 --- a/tests/e2e/features/environment.py +++ b/tests/e2e/features/environment.py @@ -18,13 +18,13 @@ from behave.runner import Context from tests.e2e.features.steps.common import ( - get_llama_stack_hostname, - get_llama_stack_port, + get_ogx_hostname, + get_ogx_port, reset_active_lightspeed_stack_config_basename, ) from tests.e2e.features.steps.health import ( get_ogx_was_running, - reset_llama_stack_disrupt_once_tracking, + reset_ogx_disrupt_once_tracking, reset_ogx_was_running, ) from tests.e2e.features.steps.tls import ( @@ -159,10 +159,10 @@ def _ensure_prow_port_forward(context: Context) -> None: restart-port-forward to re-establish the tunnel before the scenario runs. Treat HTTP 503 like 200/401 here: it means the tunnel reached Lightspeed and the - app responded. ``ogx_disrupted`` leaves Llama stopped on purpose; readiness + app responded. ``ogx_disrupted`` leaves OGX stopped on purpose; readiness then returns 503. Previously we treated 503 as a dead tunnel and ran - ``restart-lightspeed``, which restores Llama via e2e-ops and breaks later scenarios - that skip disruption (once-per-feature) while expecting Llama to stay down. + ``restart-lightspeed``, which restores OGX via e2e-ops and breaks later scenarios + that skip disruption (once-per-feature) while expecting OGX to stay down. """ host = os.getenv("E2E_LSC_HOSTNAME", "localhost") port = os.getenv("E2E_LSC_PORT", "8080") @@ -187,7 +187,7 @@ def _ensure_prow_port_forward(context: Context) -> None: # Port-forward alone failed — the pod itself may be dead (e.g. OGX # was never restored after a disruption feature). Attempt a full restart, - # which also checks Llama health before recreating LCS. + # which also checks OGX health before recreating LCS. print("[before_scenario] Port-forward failed; attempting full pod restart...") try: result = run_e2e_ops("restart-lightspeed", timeout=200) @@ -222,7 +222,7 @@ def before_scenario(context: Context, scenario: Scenario) -> None: # Skip scenarios that require separate OGX container in library mode if context.is_library_mode and "skip-in-library-mode" in scenario.effective_tags: - scenario.skip("Skipped in library mode (no separate llama-stack container)") + scenario.skip("Skipped in library mode (no separate OGX container)") return # Skip scenarios that rely on a non-default BYOK store. Only library mode @@ -232,7 +232,7 @@ def before_scenario(context: Context, scenario: Scenario) -> None: if not context.is_library_mode and "skip-in-server-mode" in scenario.effective_tags: scenario.skip( "Skipped in server mode (feature-specific BYOK store is not loaded " - "into the external llama-stack)" + "into the external OGX service)" ) return @@ -255,8 +255,8 @@ def before_scenario(context: Context, scenario: Scenario) -> None: # Clear shield unregister state from previous scenarios (see ``shields_are_disabled_for_scenario``). for _attr in ( "shields_disabled_for_scenario", - "llama_guard_provider_id", - "llama_guard_provider_shield_id", + "ogx_guard_provider_id", + "ogx_guard_provider_shield_id", ): if hasattr(context, _attr): delattr(context, _attr) @@ -302,7 +302,7 @@ def after_scenario(context: Context, scenario: Scenario) -> None: - is_library_mode (bool): whether tests run in library mode. - ogx_was_running (bool, optional): whether OGX was running before the scenario. - - hostname_llama, port_llama (str/int, optional): host and port + - hostname_ogx, port_ogx (str/int, optional): host and port used for the OGX health check. scenario (Scenario): Behave scenario (unused; shield restore uses context flags). """ @@ -316,8 +316,8 @@ def after_scenario(context: Context, scenario: Scenario) -> None: # Re-register shield if ``Given shields are disabled for this scenario`` unregistered it. if getattr(context, "shields_disabled_for_scenario", False): - provider_id = getattr(context, "llama_guard_provider_id", None) - provider_shield_id = getattr(context, "llama_guard_provider_shield_id", None) + provider_id = getattr(context, "ogx_guard_provider_id", None) + provider_shield_id = getattr(context, "ogx_guard_provider_shield_id", None) if provider_id is not None and provider_shield_id is not None: try: register_shield( @@ -332,10 +332,10 @@ def after_scenario(context: Context, scenario: Scenario) -> None: def _print_ogx_diagnostics() -> None: """Print container state, health, and recent logs to diagnose why OGX did not recover.""" - print("--- llama-stack diagnostics ---") + print("--- ogx diagnostics ---") for label, cmd in [ - ("State", ["docker", "inspect", "--format={{.State}}", "llama-stack"]), - ("Health", ["docker", "inspect", "--format={{.State.Health}}", "llama-stack"]), + ("State", ["docker", "inspect", "--format={{.State}}", "ogx"]), + ("Health", ["docker", "inspect", "--format={{.State.Health}}", "ogx"]), ]: try: r = subprocess.run( @@ -346,7 +346,7 @@ def _print_ogx_diagnostics() -> None: print(f" {label}: (inspect timed out)") try: r = subprocess.run( - ["docker", "logs", "--tail", "40", "llama-stack"], + ["docker", "logs", "--tail", "40", "ogx"], capture_output=True, text=True, timeout=10, @@ -361,10 +361,10 @@ def _print_ogx_diagnostics() -> None: print("--- end diagnostics ---") -def _restore_llama_stack() -> None: +def _restore_ogx_service() -> None: """Restore OGX connection after disruption.""" if is_prow_environment(): - # Recreate llama pod, then restart LCS so in-process clients reconnect (Llama IP/pod changed). + # Recreate OGX pod, then restart LCS so in-process clients reconnect (OGX IP/pod changed). try: restore_ogx_pod() except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e: @@ -380,7 +380,7 @@ def _restore_llama_stack() -> None: "✓ Prow: OGX restored and lightspeed-stack restarted " "for clean reconnect" ) - reset_llama_stack_disrupt_once_tracking() + reset_ogx_disrupt_once_tracking() return except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e: last_lcs_err = e @@ -398,9 +398,7 @@ def _restore_llama_stack() -> None: try: # Start the OGX container again - subprocess.run( - ["docker", "start", "llama-stack"], check=True, capture_output=True - ) + subprocess.run(["docker", "start", "ogx"], check=True, capture_output=True) # Wait for the service to be healthy print("Restoring OGX connection...") @@ -411,10 +409,10 @@ def _restore_llama_stack() -> None: [ "docker", "exec", - "llama-stack", + "ogx", "curl", "-sf", - f"http://{get_llama_stack_hostname()}:{get_llama_stack_port()}/v1/health", + f"http://{get_ogx_hostname()}:{get_ogx_port()}/v1/health", ], capture_output=True, timeout=5, @@ -422,7 +420,7 @@ def _restore_llama_stack() -> None: ) if result.returncode == 0: print("✓ OGX connection restored successfully") - reset_llama_stack_disrupt_once_tracking() + reset_ogx_disrupt_once_tracking() break except subprocess.TimeoutExpired: print( @@ -468,8 +466,8 @@ def before_feature(context: Context, feature: Feature) -> None: context.feature_config = None context.scenario_lightspeed_override_active = False context.active_lightspeed_stack_config_basename = None - # One real Llama disruption per feature (module-level flag; survives context resets) - reset_llama_stack_disrupt_once_tracking() + # One real OGX disruption per feature (module-level flag; survives context resets) + reset_ogx_disrupt_once_tracking() if feature.filename and is_tls_feature_file(feature.filename): reset_tls_prow_state() prepare_tls_feature_entry_on_prow(feature.filename) @@ -510,7 +508,7 @@ def after_feature(context: Context, feature: Feature) -> None: # Read from module-level state — Behave clears custom context attributes # between scenarios, so context.ogx_was_running is unreliable here. if get_ogx_was_running(): - _restore_llama_stack() + _restore_ogx_service() reset_ogx_was_running() if getattr(context, "feedback_e2e_conversation_cleanup", False): @@ -531,7 +529,7 @@ def after_feature(context: Context, feature: Feature) -> None: switch_config(backup_path) remove_config_backup(backup_path) if not context.is_library_mode: - restart_container("llama-stack") + restart_container("ogx") restart_container("lightspeed-stack") reset_active_lightspeed_stack_config_basename() else: diff --git a/tests/e2e/features/ogx_disrupted.feature b/tests/e2e/features/ogx_disrupted.feature index 32fbee99e..73fa4da85 100644 --- a/tests/e2e/features/ogx_disrupted.feature +++ b/tests/e2e/features/ogx_disrupted.feature @@ -1,7 +1,7 @@ @skip-in-library-mode @Authorized -Feature: Llama Stack connection disrupted +Feature: OGX connection disrupted - End-to-end scenarios that stop the Llama Stack container (or simulate disconnect) and + End-to-end scenarios that stop the OGX container (or simulate disconnect) and assert degraded responses (503, readiness, etc.). Config order matches test_list.txt: default stack, then noop-token (query/conversations/…), then rbac (rlsapi errors). Skipped in library mode. @@ -18,11 +18,11 @@ Feature: Llama Stack connection disrupted # --- @cfg_default --- @cfg_default - Scenario: Check if models endpoint reports error when llama-stack is unreachable + Scenario: Check if models endpoint reports error when ogx is unreachable Given The service uses the lightspeed-stack-default.yaml configuration And The service is restarted Given The system is in default state - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I access REST API endpoint "models" using HTTP GET method Then The status code of the response is 503 And The body of the response is the following @@ -32,11 +32,11 @@ Feature: Llama Stack connection disrupted @cfg_default - Scenario: Check if service report proper readiness state when llama stack is not available + Scenario: Check if service report proper readiness state when OGX is not available Given The service uses the lightspeed-stack-default.yaml configuration And The service is restarted Given The system is in default state - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I access endpoint "readiness" using HTTP GET method Then The status code of the response is 503 And The body of the response, ignoring the "providers" field, is the following @@ -46,11 +46,11 @@ Feature: Llama Stack connection disrupted @cfg_default - Scenario: Check if service report proper liveness state even when llama stack is not available + Scenario: Check if service report proper liveness state even when OGX is not available Given The service uses the lightspeed-stack-default.yaml configuration And The service is restarted Given The system is in default state - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I access endpoint "liveness" using HTTP GET method Then The status code of the response is 200 And The body of the response is the following @@ -60,10 +60,10 @@ Feature: Llama Stack connection disrupted @cfg_default - Scenario: Check if info endpoint reports error when llama-stack connection is not working + Scenario: Check if info endpoint reports error when ogx connection is not working Given The service uses the lightspeed-stack-default.yaml configuration And The service is restarted - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I access REST API endpoint "info" using HTTP GET method Then The status code of the response is 503 And The body of the response is the following @@ -75,10 +75,10 @@ Feature: Llama Stack connection disrupted # --- @cfg_default (noop auth; tools list needs no bearer) --- @cfg_default - Scenario: Check if tools endpoint reports error when llama-stack is unreachable + Scenario: Check if tools endpoint reports error when ogx is unreachable Given The service uses the lightspeed-stack-default.yaml configuration And The service is restarted - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I access REST API endpoint "tools" using HTTP GET method Then The status code of the response is 503 And The body of the response is the following @@ -90,12 +90,12 @@ Feature: Llama Stack connection disrupted # --- lightspeed-stack-authorized.yaml (aligned with query, responses, conversations, …) --- @cfg_authorized - Scenario: Check if LLM responds for query request with error for inability to connect to llama-stack - Given Llama Stack is restarted + Scenario: Check if LLM responds for query request with error for inability to connect to ogx + Given OGX is restarted And The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I use "query" to ask question with authorization header """ {"query": "Say hello"} @@ -105,11 +105,11 @@ Feature: Llama Stack connection disrupted @cfg_authorized - Scenario: Responses returns error when unable to connect to llama-stack + Scenario: Responses returns error when unable to connect to ogx Given The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted Given The system is in default state - And The llama-stack connection is disrupted + And The ogx connection is disrupted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva When I use "responses" to ask question with authorization header """ @@ -120,11 +120,11 @@ Feature: Llama Stack connection disrupted @cfg_authorized - Scenario: Streaming responses returns error when unable to connect to llama-stack + Scenario: Streaming responses returns error when unable to connect to ogx Given The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I use "responses" to ask question with authorization header """ {"input": "Say hello", "model": "{PROVIDER}/{MODEL}", "stream": true} @@ -134,33 +134,33 @@ Feature: Llama Stack connection disrupted @cfg_authorized - Scenario: Check if rags endpoint fails when llama-stack is unavailable + Scenario: Check if rags endpoint fails when ogx is unavailable Given The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I access REST API endpoint rags using HTTP GET method Then The status code of the response is 503 And The body of the response contains Unable to connect to OGX @cfg_authorized - Scenario: Check if prompts list endpoint fails when llama-stack is unavailable + Scenario: Check if prompts list endpoint fails when ogx is unavailable Given The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I access REST API endpoint "prompts" using HTTP GET method Then The status code of the response is 503 And The body of the response contains Unable to connect to OGX @cfg_authorized - Scenario: Check if prompts create endpoint fails when llama-stack is unavailable + Scenario: Check if prompts create endpoint fails when ogx is unavailable Given The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I access REST API endpoint "prompts" using HTTP POST method """ {"prompt": "Summarize: {{text}}", "variables": ["text"]} @@ -170,22 +170,22 @@ Feature: Llama Stack connection disrupted @cfg_authorized - Scenario: Check if prompts get by id endpoint fails when llama-stack is unavailable + Scenario: Check if prompts get by id endpoint fails when ogx is unavailable Given The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I access REST API endpoint "prompts/pmpt_5c76d7f7c633ef97477adeb2f642150d8d08e8a6526e9909" using HTTP GET method Then The status code of the response is 503 And The body of the response contains Unable to connect to OGX @cfg_authorized - Scenario: Check if prompts update endpoint fails when llama-stack is unavailable + Scenario: Check if prompts update endpoint fails when ogx is unavailable Given The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I access REST API endpoint "prompts/pmpt_5c76d7f7c633ef97477adeb2f642150d8d08e8a6526e9909" using HTTP PUT method """ {"prompt": "Summarize in bullets: {{text}}", "version": 1, "set_as_default": true, "variables": ["text"]} @@ -195,19 +195,19 @@ Feature: Llama Stack connection disrupted @cfg_authorized - Scenario: Check if prompts delete endpoint fails when llama-stack is unavailable + Scenario: Check if prompts delete endpoint fails when ogx is unavailable Given The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I access REST API endpoint "prompts/pmpt_5c76d7f7c633ef97477adeb2f642150d8d08e8a6526e9909" using HTTP DELETE method Then The status code of the response is 503 And The body of the response contains Unable to connect to OGX @cfg_authorized - Scenario: Check if conversations/{conversation_id} GET endpoint fails when llama-stack is unavailable - Given Llama Stack is restarted + Scenario: Check if conversations/{conversation_id} GET endpoint fails when ogx is unavailable + Given OGX is restarted And The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva @@ -217,15 +217,15 @@ Feature: Llama Stack connection disrupted """ And The status code of the response is 200 And I store conversation details - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I use REST API conversation endpoint with conversation_id from above using HTTP GET method Then The status code of the response is 503 And The body of the response contains Unable to connect to OGX @cfg_authorized - Scenario: Check if conversations/{conversation_id} DELETE endpoint fails when llama-stack is unavailable - Given Llama Stack is restarted + Scenario: Check if conversations/{conversation_id} DELETE endpoint fails when ogx is unavailable + Given OGX is restarted And The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva @@ -235,15 +235,15 @@ Feature: Llama Stack connection disrupted """ And The status code of the response is 200 And I store conversation details - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I use REST API conversation endpoint with conversation_id from above using HTTP DELETE method Then The status code of the response is 503 And The body of the response contains Unable to connect to OGX @cfg_authorized - Scenario: Check conversations/{conversation_id} works when llama-stack is down - Given Llama Stack is restarted + Scenario: Check conversations/{conversation_id} works when ogx is down + Given OGX is restarted And The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva @@ -254,7 +254,7 @@ Feature: Llama Stack connection disrupted """ And The status code of the response is 200 And I store conversation details - And The llama-stack connection is disrupted + And The ogx connection is disrupted And REST API service prefix is /v2 When I access REST API endpoint "conversations" using HTTP GET method Then The status code of the response is 200 @@ -268,8 +268,8 @@ Feature: Llama Stack connection disrupted # --- still @cfg_authorized (noop-with-token; not RBAC) --- @cfg_authorized - Scenario: V2 conversations DELETE endpoint works even when llama-stack is down - Given Llama Stack is restarted + Scenario: V2 conversations DELETE endpoint works even when ogx is down + Given OGX is restarted And The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva @@ -280,7 +280,7 @@ Feature: Llama Stack connection disrupted """ And The status code of the response is 200 And I store conversation details - And The llama-stack connection is disrupted + And The ogx connection is disrupted And REST API service prefix is /v2 When I use REST API conversation endpoint with conversation_id from above using HTTP DELETE method Then The status code of the response is 200 @@ -297,12 +297,12 @@ Feature: Llama Stack connection disrupted # --- lightspeed-stack-rbac.yaml (aligned with rbac.feature / rlsapi_v1_errors.feature) --- @RBAC @cfg_rbac - Scenario: Returns 503 when llama-stack connection is broken - Given Llama Stack is restarted + Scenario: Returns 503 when ogx connection is broken + Given OGX is restarted And The service uses the lightspeed-stack-rbac.yaml configuration And The service is restarted And I authenticate as "user" user - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I use "infer" to ask question with authorization header """ {"question": "How do I list files?"} diff --git a/tests/e2e/features/okp_rag.feature b/tests/e2e/features/okp_rag.feature index fb5c6654f..64211c258 100644 --- a/tests/e2e/features/okp_rag.feature +++ b/tests/e2e/features/okp_rag.feature @@ -38,7 +38,7 @@ Feature: OKP(Solr) RAG retrieval tests Scenario: Online mode streaming query with inline RAG returns referenced_documents Given The service uses the lightspeed-stack-okp-online.yaml configuration - And Llama Stack is restarted + And OGX is restarted And The service is restarted When I use "streaming_query" to ask question with authorization header """ diff --git a/tests/e2e/features/proxy.feature b/tests/e2e/features/proxy.feature index 17c8be4f5..813011e79 100644 --- a/tests/e2e/features/proxy.feature +++ b/tests/e2e/features/proxy.feature @@ -1,12 +1,12 @@ @cfg_default @skip-in-library-mode @skip-in-prow -Feature: Proxy and TLS networking tests for Llama Stack providers +Feature: Proxy and TLS networking tests for OGX providers - Verify that the Lightspeed Stack works correctly when Llama Stack's + Verify that the Lightspeed Stack works correctly when OGX's remote inference providers are configured with proxy and TLS settings via the run.yaml NetworkConfig. Query bodies use shield_ids: [] because Llama Guard moderation can issue - separate provider calls inside Llama Stack that may not inherit the same + separate provider calls inside OGX that may not inherit the same proxy/TLS CA trust as the scenario's remote inference provider. Background: @@ -16,7 +16,7 @@ Feature: Proxy and TLS networking tests for Llama Stack providers And the Lightspeed stack configuration directory is "tests/e2e/configuration" And The service uses the lightspeed-stack-default.yaml configuration And The service is restarted - And The original Llama Stack config is restored if modified + And The original OGX config is restored if modified # --- AC1: Tunnel proxy routing --- @@ -24,8 +24,8 @@ Feature: Proxy and TLS networking tests for Llama Stack providers @TunnelProxy Scenario: LLM traffic is routed through a configured tunnel proxy Given A tunnel proxy is running on port 8888 - And Llama Stack is configured to route inference through the tunnel proxy - And Llama Stack is restarted + And OGX is configured to route inference through the tunnel proxy + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -40,8 +40,8 @@ Feature: Proxy and TLS networking tests for Llama Stack providers @TunnelProxy Scenario: LLM query fails gracefully when proxy is unreachable - Given Llama Stack is configured to route inference through proxy "http://127.0.0.1:19999" - And Llama Stack is restarted + Given OGX is configured to route inference through proxy "http://127.0.0.1:19999" + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -56,8 +56,8 @@ Feature: Proxy and TLS networking tests for Llama Stack providers @InterceptionProxy @flaky Scenario: LLM traffic works through interception proxy with correct CA Given An interception proxy with trustme CA is running on port 8889 - And Llama Stack is configured to route inference through the interception proxy with CA cert - And Llama Stack is restarted + And OGX is configured to route inference through the interception proxy with CA cert + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -69,8 +69,8 @@ Feature: Proxy and TLS networking tests for Llama Stack providers @InterceptionProxy Scenario: LLM query fails when interception proxy CA is not provided Given An interception proxy with trustme CA is running on port 8890 - And Llama Stack is configured to route inference through the interception proxy without CA cert - And Llama Stack is restarted + And OGX is configured to route inference through the interception proxy without CA cert + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -84,8 +84,8 @@ Feature: Proxy and TLS networking tests for Llama Stack providers @TLSVersion @flaky Scenario: TLS minimum version TLSv1.2 is respected - Given Llama Stack is configured with minimum TLS version "TLSv1.2" - And Llama Stack is restarted + Given OGX is configured with minimum TLS version "TLSv1.2" + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -95,8 +95,8 @@ Feature: Proxy and TLS networking tests for Llama Stack providers @TLSVersion @flaky Scenario: TLS minimum version TLSv1.3 is respected - Given Llama Stack is configured with minimum TLS version "TLSv1.3" - And Llama Stack is restarted + Given OGX is configured with minimum TLS version "TLSv1.3" + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -106,8 +106,8 @@ Feature: Proxy and TLS networking tests for Llama Stack providers @TLSCipher @flaky Scenario: Custom cipher suite configuration is respected - Given Llama Stack is configured with ciphers "ECDHE+AESGCM:DHE+AESGCM" - And Llama Stack is restarted + Given OGX is configured with ciphers "ECDHE+AESGCM:DHE+AESGCM" + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ diff --git a/tests/e2e/features/shields.feature b/tests/e2e/features/shields.feature index 5914b9012..1d8981627 100644 --- a/tests/e2e/features/shields.feature +++ b/tests/e2e/features/shields.feature @@ -2,7 +2,7 @@ Feature: Shields endpoint tests Tests for the LCORE-owned GET /v1/shields catalog endpoint. These shields (question_validity, redaction) are configured directly in - lightspeed-stack.yaml; they are not Llama Stack / OGX Safety API resources. + lightspeed-stack.yaml; they are not OGX Safety API resources. See docs/user_doc/shields_guide.md for the full shield configuration and runtime-behavior reference. diff --git a/tests/e2e/features/steps/common.py b/tests/e2e/features/steps/common.py index b11e292f3..ba49db388 100644 --- a/tests/e2e/features/steps/common.py +++ b/tests/e2e/features/steps/common.py @@ -23,7 +23,7 @@ # Behave clears user attributes on ``context`` between scenarios; store # OGX endpoint info at module level so ``after_feature`` can see it. -_llama_stack_endpoint: dict[str, str] = {"hostname": "localhost", "port": "8321"} +_ogx_endpoint: dict[str, str] = {"hostname": "localhost", "port": "8321"} def reset_active_lightspeed_stack_config_basename() -> None: @@ -40,14 +40,14 @@ def get_active_lightspeed_stack_config_basename() -> Optional[str]: return _active_lightspeed_stack_config_basename["basename"] -def get_llama_stack_hostname() -> str: +def get_ogx_hostname() -> str: """Return the OGX hostname surviving per-scenario context clearing.""" - return _llama_stack_endpoint["hostname"] + return _ogx_endpoint["hostname"] -def get_llama_stack_port() -> str: +def get_ogx_port() -> str: """Return the OGX port surviving per-scenario context clearing.""" - return _llama_stack_endpoint["port"] + return _ogx_endpoint["port"] @given("The service is started locally") @@ -65,12 +65,12 @@ def service_is_started_locally(context: Context) -> None: context.hostname = os.getenv("E2E_LSC_HOSTNAME", "localhost") context.port = os.getenv("E2E_LSC_PORT", "8080") if is_prow_environment(): - context.hostname_llama = os.getenv("E2E_LLAMA_HOSTNAME", "localhost") + context.hostname_ogx = os.getenv("E2E_OGX_HOSTNAME", "localhost") else: - context.hostname_llama = "localhost" - context.port_llama = os.getenv("E2E_LLAMA_PORT", "8321") - _llama_stack_endpoint["hostname"] = context.hostname_llama - _llama_stack_endpoint["port"] = context.port_llama + context.hostname_ogx = "localhost" + context.port_ogx = os.getenv("E2E_OGX_PORT", "8321") + _ogx_endpoint["hostname"] = context.hostname_ogx + _ogx_endpoint["port"] = context.port_ogx @given('the Lightspeed stack configuration directory is "{directory}"') @@ -120,7 +120,7 @@ def configure_service(context: Context, config_name: str) -> None: """ config_name = config_name.strip() if _active_lightspeed_stack_config_basename["basename"] == config_name: - # MCP reset or llama disrupt: do not skip the next restart. + # MCP reset or OGX disrupt: do not skip the next restart. if getattr(context, "force_lightspeed_restart_after_mcp_config_reset", False): context.lightspeed_stack_skip_restart = False context.force_lightspeed_restart_after_mcp_config_reset = False @@ -201,19 +201,19 @@ def restart_service(context: Context) -> None: restart_container("lightspeed-stack") -@given("The service is restarted without restoring llama-stack") -def restart_service_without_restoring_llama(context: Context) -> None: - """Restart LCS while leaving llama disrupted (degraded-mode startup e2e). +@given("The service is restarted without restoring ogx") +def restart_service_without_restoring_ogx(context: Context) -> None: + """Restart LCS while leaving OGX disrupted (degraded-mode startup e2e). - On Prow/Konflux, the default ``restart-lightspeed`` path restores llama when + On Prow/Konflux, the default ``restart-lightspeed`` path restores OGX when it is unhealthy so LCS can come up. Degraded-mode scenarios need the - opposite: LCS must boot with llama still down. Docker Compose already + opposite: LCS must boot with OGX still down. Docker Compose already restarts only the LCS container, so this matches local server-mode behavior. """ if getattr(context, "lightspeed_stack_skip_restart", False): context.lightspeed_stack_skip_restart = False return - restart_lightspeed_stack_service(skip_llama_restore=True, wait_http=False) + restart_lightspeed_stack_service(skip_ogx_restore=True, wait_http=False) @given("The system is in default state") diff --git a/tests/e2e/features/steps/health.py b/tests/e2e/features/steps/health.py index 4afbc0dd4..7409da4bd 100644 --- a/tests/e2e/features/steps/health.py +++ b/tests/e2e/features/steps/health.py @@ -11,7 +11,7 @@ # Behave may clear user attributes on ``context`` between scenarios; keep this # in module scope so "disrupt once per feature" survives per-scenario resets. # Mutate one dict entry so we need not reassign a module-level bool (no global). -_llama_stack_disrupt_once: dict[str, bool] = {"applied": False} +_ogx_disrupt_once: dict[str, bool] = {"applied": False} # Behave clears user attributes on ``context`` between scenarios; store # ``was_running`` at module level so ``after_feature`` can still see it. @@ -28,40 +28,34 @@ def reset_ogx_was_running() -> None: _ogx_was_running["value"] = False -def reset_llama_stack_disrupt_once_tracking() -> None: +def reset_ogx_disrupt_once_tracking() -> None: """Reset before each feature; see ``environment.before_feature``.""" - _llama_stack_disrupt_once["applied"] = False + _ogx_disrupt_once["applied"] = False _ogx_was_running["value"] = False -def _force_lightspeed_restart_after_llama_disrupt(context: Context) -> None: +def _force_lightspeed_restart_after_ogx_disrupt(context: Context) -> None: """Do not skip the next Lightspeed restart after OGX is disrupted.""" context.force_lightspeed_restart_after_mcp_config_reset = True context.lightspeed_stack_skip_restart = False -def _force_lightspeed_restart_after_llama_disrupt(context: Context) -> None: - """Do not skip the next Lightspeed restart after Llama is disrupted.""" - context.force_lightspeed_restart_after_mcp_config_reset = True - context.lightspeed_stack_skip_restart = False - - -@given("The llama-stack connection is disrupted") -def llama_stack_connection_broken(context: Context) -> None: - """Break llama_stack connection by stopping the container. +@given("The ogx connection is disrupted") +def ogx_connection_broken(context: Context) -> None: + """Break the OGX connection by stopping the container. Disrupts the OGX service by stopping its Docker container and records whether it was running. The real disruption runs only once per feature until OGX is running again: the first invocation performs Docker/Prow disruption; later invocations no-op. - ``reset_llama_stack_disrupt_once_tracking`` clears the skip flag from + ``reset_ogx_disrupt_once_tracking`` clears the skip flag from ``before_feature`` and after OGX is restored (``restart_container``, - ``_restore_llama_stack``) so the next disrupt step stops the container again. + ``_restore_ogx_service``) so the next disrupt step stops the container again. Tracking uses module state (not ``context`` alone) because Behave can clear custom attributes on ``context`` between scenarios. - Checks whether the Docker container named "llama-stack" is running; if it + Checks whether the Docker container named "ogx" is running; if it is, stops the container, waits briefly for the disruption to take effect, and sets `context.ogx_was_running` to True so callers can restore state later. If the container is not running, the flag remains False. On @@ -73,9 +67,9 @@ def llama_stack_connection_broken(context: Context) -> None: context (behave.runner.Context): Behave context used to store `ogx_was_running` and share state between steps. """ - if _llama_stack_disrupt_once["applied"]: + if _ogx_disrupt_once["applied"]: print("OGX disruption skipped (already applied once this feature)") - _force_lightspeed_restart_after_llama_disrupt(context) + _force_lightspeed_restart_after_ogx_disrupt(context) return # Store original state for restoration (only on the real disruption path). @@ -85,19 +79,19 @@ def llama_stack_connection_broken(context: Context) -> None: _ogx_was_running["value"] = False if is_prow_environment(): - from tests.e2e.utils.prow_utils import disrupt_llama_stack_pod + from tests.e2e.utils.prow_utils import disrupt_ogx_pod - was_running = disrupt_llama_stack_pod() + was_running = disrupt_ogx_pod() context.ogx_was_running = was_running _ogx_was_running["value"] = was_running - _llama_stack_disrupt_once["applied"] = True - _force_lightspeed_restart_after_llama_disrupt(context) + _ogx_disrupt_once["applied"] = True + _force_lightspeed_restart_after_ogx_disrupt(context) return # Docker-based disruption try: result = subprocess.run( - ["docker", "inspect", "-f", "{{.State.Running}}", "llama-stack"], + ["docker", "inspect", "-f", "{{.State.Running}}", "ogx"], capture_output=True, text=True, check=True, @@ -106,9 +100,7 @@ def llama_stack_connection_broken(context: Context) -> None: if result.stdout.strip(): context.ogx_was_running = True _ogx_was_running["value"] = True - subprocess.run( - ["docker", "stop", "llama-stack"], check=True, capture_output=True - ) + subprocess.run(["docker", "stop", "ogx"], check=True, capture_output=True) # Wait a moment for the connection to be fully disrupted time.sleep(2) @@ -121,5 +113,5 @@ def llama_stack_connection_broken(context: Context) -> None: print(f"Warning: Could not disrupt OGX connection: {e}") return - _llama_stack_disrupt_once["applied"] = True - _force_lightspeed_restart_after_llama_disrupt(context) + _ogx_disrupt_once["applied"] = True + _force_lightspeed_restart_after_ogx_disrupt(context) diff --git a/tests/e2e/features/steps/proxy.py b/tests/e2e/features/steps/proxy.py index eb2e3ced3..ddb8d4c50 100644 --- a/tests/e2e/features/steps/proxy.py +++ b/tests/e2e/features/steps/proxy.py @@ -34,9 +34,9 @@ from tests.e2e.proxy.tunnel_proxy import DEFAULT_PROXY_PORT from tests.e2e.utils.ogx_config_utils import ( backup_ogx_config, - load_llama_config, - restore_llama_config_if_modified, - write_llama_config, + load_ogx_config, + restore_ogx_config_if_modified, + write_ogx_config, ) from tests.e2e.utils.prow_utils import get_namespace, run_e2e_ops from tests.e2e.utils.utils import ( @@ -55,12 +55,12 @@ def _is_docker_mode() -> bool: if is_prow_environment(): return False result = subprocess.run( - ["docker", "ps", "--filter", "name=llama-stack", "--format", "{{.Names}}"], + ["docker", "ps", "--filter", "name=ogx", "--format", "{{.Names}}"], capture_output=True, text=True, check=False, ) - return "llama-stack" in result.stdout + return "ogx" in result.stdout def _host_special_dns_from_container(hostname: str) -> Optional[str]: @@ -89,7 +89,7 @@ def _host_special_dns_from_container(hostname: str) -> Optional[str]: [ "docker", "exec", - "llama-stack", + "ogx", "python3", "-c", probe, @@ -188,11 +188,11 @@ def _fetch_cluster_interception_proxy_stats() -> dict[str, Any]: return stats -_INTERCEPTION_CA_LLAMA_PATH = "/tmp/interception-proxy-ca.pem" +_INTERCEPTION_CA_OGX_PATH = "/tmp/interception-proxy-ca.pem" def _sync_interception_proxy_ca_secret() -> None: - """Publish trustme CA to Secret ``e2e-interception-proxy-ca`` (mounted by llama pod).""" + """Publish trustme CA to Secret ``e2e-interception-proxy-ca`` (mounted by OGX pod).""" result = run_e2e_ops("sync-interception-proxy-ca-secret", timeout=90) print(result.stdout, end="") if result.returncode != 0: @@ -298,7 +298,7 @@ def _stop_proxy(context: Context, attr: str, loop_attr: str) -> None: delattr(context, loop_attr) -@given("The original Llama Stack config is restored if modified") +@given("The original OGX config is restored if modified") def restore_if_modified(context: Context) -> None: """Restore original run.yaml if a previous scenario modified it. @@ -309,30 +309,30 @@ def restore_if_modified(context: Context) -> None: # Stop any leftover proxy servers from previous scenario _stop_proxy(context, "tunnel_proxy", "proxy_loop") _stop_proxy(context, "interception_proxy", "interception_proxy_loop") - os.environ.pop("E2E_COPY_INTERCEPTION_CA_TO_LLAMA", None) - os.environ.pop("E2E_COPY_MOCK_TLS_CERTS_TO_LLAMA", None) - if hasattr(context, "needs_interception_ca_on_llama"): - delattr(context, "needs_interception_ca_on_llama") + os.environ.pop("E2E_COPY_INTERCEPTION_CA_TO_OGX", None) + os.environ.pop("E2E_COPY_MOCK_TLS_CERTS_TO_OGX", None) + if hasattr(context, "needs_interception_ca_on_ogx"): + delattr(context, "needs_interception_ca_on_ogx") - if restore_llama_config_if_modified(): + if restore_ogx_config_if_modified(): print("Restoring original OGX config from backup...") # --- Service Restart Steps --- -@given("Llama Stack is restarted") +@given("OGX is restarted") def restart_ogx(context: Context) -> None: """Restart the OGX container.""" from tests.e2e.features.steps.tls import ( is_tls_configuration_feature, - restart_llama_for_tls_feature, + restart_ogx_for_tls_feature, ) if is_tls_configuration_feature(context): - restart_llama_for_tls_feature(context) + restart_ogx_for_tls_feature(context) return - restart_container("llama-stack") + restart_container("ogx") @given("Lightspeed Stack is restarted") @@ -392,8 +392,8 @@ def run_proxy() -> None: time.sleep(1) -@given("Llama Stack is configured to route inference through the tunnel proxy") -def configure_llama_tunnel_proxy(context: Context) -> None: +@given("OGX is configured to route inference through the tunnel proxy") +def configure_ogx_tunnel_proxy(context: Context) -> None: """Modify run.yaml with proxy config pointing to the tunnel proxy.""" backup_ogx_config() if is_prow_environment(): @@ -402,7 +402,7 @@ def configure_llama_tunnel_proxy(context: Context) -> None: proxy = context.tunnel_proxy proxy_port = proxy.port proxy_host = _get_proxy_host(context.is_docker_mode) - config = load_llama_config() + config = load_ogx_config() provider = _find_inference_provider(context, config) if "config" not in provider: @@ -413,14 +413,14 @@ def configure_llama_tunnel_proxy(context: Context) -> None: } } - write_llama_config(config) + write_ogx_config(config) -@given('Llama Stack is configured to route inference through proxy "{proxy_url}"') -def configure_llama_unreachable_proxy(context: Context, proxy_url: str) -> None: +@given('OGX is configured to route inference through proxy "{proxy_url}"') +def configure_ogx_unreachable_proxy(context: Context, proxy_url: str) -> None: """Modify run.yaml with a proxy URL (may be unreachable).""" backup_ogx_config() - config = load_llama_config() + config = load_ogx_config() provider = _find_inference_provider(context, config) if "config" not in provider: @@ -431,7 +431,7 @@ def configure_llama_unreachable_proxy(context: Context, proxy_url: str) -> None: } } - write_llama_config(config) + write_ogx_config(config) # --- Interception Proxy Steps --- @@ -444,7 +444,7 @@ def start_interception_proxy(context: Context, port: int) -> None: cluster_port = _cluster_interception_proxy_port(port) context.interception_proxy = None context.cluster_interception_proxy_port = cluster_port - context.ca_cert_path_for_config = _INTERCEPTION_CA_LLAMA_PATH + context.ca_cert_path_for_config = _INTERCEPTION_CA_OGX_PATH _deploy_cluster_interception_proxy() print( f"Using in-cluster interception proxy at " @@ -466,7 +466,7 @@ def start_interception_proxy(context: Context, port: int) -> None: if context.is_docker_mode: container_cert_path = "/tmp/interception-proxy-ca.pem" subprocess.run( - ["docker", "cp", str(ca_cert_path), f"llama-stack:{container_cert_path}"], + ["docker", "cp", str(ca_cert_path), f"ogx:{container_cert_path}"], check=True, ) context.ca_cert_path_for_config = container_cert_path @@ -499,15 +499,15 @@ def run_proxy() -> None: @given( - "Llama Stack is configured to route inference through " + "OGX is configured to route inference through " "the interception proxy with CA cert" ) -def configure_llama_interception_with_ca(context: Context) -> None: +def configure_ogx_interception_with_ca(context: Context) -> None: """Modify run.yaml with interception proxy and CA cert config.""" backup_ogx_config() - context.needs_interception_ca_on_llama = True + context.needs_interception_ca_on_ogx = True if is_prow_environment(): - os.environ["E2E_COPY_INTERCEPTION_CA_TO_LLAMA"] = "1" + os.environ["E2E_COPY_INTERCEPTION_CA_TO_OGX"] = "1" if is_prow_environment(): proxy_port = getattr( context, "cluster_interception_proxy_port", DEFAULT_INTERCEPTION_PROXY_PORT @@ -517,7 +517,7 @@ def configure_llama_interception_with_ca(context: Context) -> None: proxy = context.interception_proxy proxy_port = proxy.port proxy_host = _get_proxy_host(context.is_docker_mode) - config = load_llama_config() + config = load_ogx_config() provider = _find_inference_provider(context, config) if "config" not in provider: @@ -532,20 +532,20 @@ def configure_llama_interception_with_ca(context: Context) -> None: }, } - write_llama_config(config) + write_ogx_config(config) if is_prow_environment(): _sync_interception_proxy_ca_secret() @given( - "Llama Stack is configured to route inference through " + "OGX is configured to route inference through " "the interception proxy without CA cert" ) -def configure_llama_interception_no_ca(context: Context) -> None: +def configure_ogx_interception_no_ca(context: Context) -> None: """Modify run.yaml with interception proxy but NO CA cert.""" backup_ogx_config() - context.needs_interception_ca_on_llama = False - os.environ.pop("E2E_COPY_INTERCEPTION_CA_TO_LLAMA", None) + context.needs_interception_ca_on_ogx = False + os.environ.pop("E2E_COPY_INTERCEPTION_CA_TO_OGX", None) if is_prow_environment(): proxy_port = getattr( context, "cluster_interception_proxy_port", DEFAULT_INTERCEPTION_PROXY_PORT @@ -555,7 +555,7 @@ def configure_llama_interception_no_ca(context: Context) -> None: proxy = context.interception_proxy proxy_port = proxy.port proxy_host = _get_proxy_host(context.is_docker_mode) - config = load_llama_config() + config = load_ogx_config() provider = _find_inference_provider(context, config) if "config" not in provider: @@ -566,17 +566,17 @@ def configure_llama_interception_no_ca(context: Context) -> None: }, } - write_llama_config(config) + write_ogx_config(config) # --- TLS Steps --- -@given('Llama Stack is configured with minimum TLS version "{version}"') -def configure_llama_tls_version(context: Context, version: str) -> None: +@given('OGX is configured with minimum TLS version "{version}"') +def configure_ogx_tls_version(context: Context, version: str) -> None: """Modify run.yaml with TLS version config.""" backup_ogx_config() - config = load_llama_config() + config = load_ogx_config() provider = _find_inference_provider(context, config) if "config" not in provider: @@ -587,14 +587,14 @@ def configure_llama_tls_version(context: Context, version: str) -> None: } } - write_llama_config(config) + write_ogx_config(config) -@given('Llama Stack is configured with ciphers "{ciphers}"') -def configure_llama_ciphers(context: Context, ciphers: str) -> None: +@given('OGX is configured with ciphers "{ciphers}"') +def configure_ogx_ciphers(context: Context, ciphers: str) -> None: """Modify run.yaml with cipher suite config.""" backup_ogx_config() - config = load_llama_config() + config = load_ogx_config() provider = _find_inference_provider(context, config) if "config" not in provider: @@ -605,7 +605,7 @@ def configure_llama_ciphers(context: Context, ciphers: str) -> None: } } - write_llama_config(config) + write_ogx_config(config) # --- Proxy Verification Steps --- diff --git a/tests/e2e/features/steps/shields.py b/tests/e2e/features/steps/shields.py index 0e1861746..1ac4f4443 100644 --- a/tests/e2e/features/steps/shields.py +++ b/tests/e2e/features/steps/shields.py @@ -27,8 +27,8 @@ def shields_are_disabled_for_scenario(context: Context) -> None: try: saved = unregister_shield("llama-guard") - context.llama_guard_provider_id = saved[0] if saved else None - context.llama_guard_provider_shield_id = saved[1] if saved else None + context.ogx_guard_provider_id = saved[0] if saved else None + context.ogx_guard_provider_shield_id = saved[1] if saved else None context.shields_disabled_for_scenario = True print("Unregistered shield llama-guard for this scenario") except Exception as e: # pylint: disable=broad-exception-caught diff --git a/tests/e2e/features/steps/tls.py b/tests/e2e/features/steps/tls.py index 7c31d9b71..a87715d2d 100644 --- a/tests/e2e/features/steps/tls.py +++ b/tests/e2e/features/steps/tls.py @@ -17,10 +17,10 @@ from tests.e2e.utils.ogx_config_utils import ( backup_ogx_config, - clear_llama_config_backup, - load_llama_config, - reset_llama_run_config_to_pipeline_default, - write_llama_config, + clear_ogx_config_backup, + load_ogx_config, + reset_ogx_run_config_to_pipeline_default, + write_ogx_config, ) from tests.e2e.utils.prow_utils import get_namespace, restart_pod, run_e2e_ops from tests.e2e.utils.utils import is_prow_environment @@ -38,10 +38,10 @@ def reset_tls_prow_state() -> None: """Reset per-feature TLS test state (call from ``before_feature``).""" - os.environ.pop("E2E_COPY_MOCK_TLS_CERTS_TO_LLAMA", None) + os.environ.pop("E2E_COPY_MOCK_TLS_CERTS_TO_OGX", None) os.environ.pop("E2E_SYNC_MOCK_TLS_CERTS", None) os.environ.pop("E2E_MOCK_TLS_INFERENCE_HOST", None) - clear_llama_config_backup() + clear_ogx_config_backup() def is_tls_feature_file(feature_filename: Optional[str]) -> bool: @@ -64,11 +64,11 @@ def prepare_tls_feature_entry_on_prow(feature_filename: Optional[str] = None) -> return label = os.path.basename(feature_filename or "tls.feature") print(f"[{label}] Prow/Konflux entry: ensure mock TLS, reset run.yaml, warm OGX...") - reset_llama_run_config_to_pipeline_default() + reset_ogx_run_config_to_pipeline_default() _ensure_cluster_mock_tls_inference() - _prepare_tls_prow_llama_restart_env() + _prepare_tls_prow_ogx_restart_env() os.environ.pop("E2E_SYNC_MOCK_TLS_CERTS", None) - restart_pod("llama-stack") + restart_pod("ogx") print(f"[{label}] Prow/Konflux entry baseline complete", flush=True) @@ -84,12 +84,12 @@ def is_tls_configuration_feature(context: Context) -> bool: return "TLS configuration" in name -def _prepare_tls_prow_llama_restart_env() -> None: - """Set env for full llama pod recreate with mock TLS certs mounted.""" - os.environ["E2E_COPY_MOCK_TLS_CERTS_TO_LLAMA"] = "1" +def _prepare_tls_prow_ogx_restart_env() -> None: + """Set env for full OGX pod recreate with mock TLS certs mounted.""" + os.environ["E2E_COPY_MOCK_TLS_CERTS_TO_OGX"] = "1" -def _restart_lightspeed_after_llama_tls(context: Context) -> None: +def _restart_lightspeed_after_ogx_tls(context: Context) -> None: """Restart LCS after OGX recreate so the in-process OGX client reconnects. TLS scenarios only change OGX run.yaml; LCS yaml is unchanged. Without this, @@ -114,12 +114,12 @@ def _restart_lightspeed_after_llama_tls(context: Context) -> None: wait_for_lightspeed_stack_http_ready() -def restart_llama_for_tls_feature(context: Context) -> None: +def restart_ogx_for_tls_feature(context: Context) -> None: """Restart OGX for TLS tests (full pod recreate on Prow/Konflux).""" from tests.e2e.utils.utils import restart_container if is_prow_environment(): - _prepare_tls_prow_llama_restart_env() + _prepare_tls_prow_ogx_restart_env() os.environ.pop("E2E_SYNC_MOCK_TLS_CERTS", None) scenario = getattr(getattr(context, "scenario", None), "name", "") or "?" feature_file = os.path.basename( @@ -129,7 +129,7 @@ def restart_llama_for_tls_feature(context: Context) -> None: f"[{feature_file}] OGX restart: full recreate scenario={scenario!r}", flush=True, ) - restart_container("llama-stack") + restart_container("ogx") def _cluster_mock_tls_inference_host() -> str: @@ -173,7 +173,7 @@ def _ensure_cluster_mock_tls_inference() -> None: """Deploy mock TLS on Prow if missing; keep one pod for the whole tls suite. ``deploy-e2e-mock-tls-inference`` copies all PEMs into Secret ``e2e-mock-tls-certs`` - once. Scenarios only change which cert path Llama uses in run.yaml. + once. Scenarios only change which cert path OGX uses in run.yaml. """ if _mock_tls_inference_pod_ready(): print("Using existing e2e-mock-tls-inference deployment") @@ -239,7 +239,7 @@ def _configure_tls(tls_config: dict[str, Any], base_url: Optional[str] = None) - base_url: Optional base URL override for the provider. """ backup_ogx_config() - config = load_llama_config() + config = load_ogx_config() provider = _ensure_tls_provider(config) provider.setdefault("config", {}).setdefault("network", {}) if base_url is not None: @@ -248,9 +248,9 @@ def _configure_tls(tls_config: dict[str, Any], base_url: Optional[str] = None) - provider["config"]["base_url"] = _mock_tls_base_url(_MOCK_TLS_PORT_TLS) provider.setdefault("config", {})["refresh_models"] = False provider["config"]["network"]["tls"] = tls_config - write_llama_config(config) + write_ogx_config(config) if is_prow_environment(): - _prepare_tls_prow_llama_restart_env() + _prepare_tls_prow_ogx_restart_env() # --- Background Steps --- @@ -270,25 +270,25 @@ def deploy_mock_tls_inference_server(context: Context) -> None: # --- TLS Configuration Steps --- -@given("Llama Stack is configured with TLS verification disabled") +@given("OGX is configured with TLS verification disabled") def configure_tls_verify_false(context: Context) -> None: """Configure run.yaml with TLS verify: false.""" _configure_tls({"verify": False}) -@given("Llama Stack is configured with CA certificate verification") +@given("OGX is configured with CA certificate verification") def configure_tls_verify_ca(context: Context) -> None: """Configure run.yaml with TLS verify: /certs/ca.crt.""" _configure_tls({"verify": "/certs/ca.crt", "min_version": "TLSv1.2"}) -@given("Llama Stack is configured with TLS verification enabled") +@given("OGX is configured with TLS verification enabled") def configure_tls_verify_true(context: Context) -> None: """Configure run.yaml with TLS verify: true (fails with self-signed certs).""" _configure_tls({"verify": True}) -@given("Llama Stack is configured with mutual TLS authentication") +@given("OGX is configured with mutual TLS authentication") def configure_tls_mtls(context: Context) -> None: """Configure run.yaml with mutual TLS (client cert and key).""" _configure_tls( @@ -302,7 +302,7 @@ def configure_tls_mtls(context: Context) -> None: ) -@given("Llama Stack is configured for mTLS without client certificate") +@given("OGX is configured for mTLS without client certificate") def configure_tls_mtls_no_client_cert(context: Context) -> None: """Configure run.yaml for mTLS port without client cert (should fail).""" _configure_tls( @@ -311,7 +311,7 @@ def configure_tls_mtls_no_client_cert(context: Context) -> None: ) -@given("Llama Stack is configured for mTLS with wrong client certificate") +@given("OGX is configured for mTLS with wrong client certificate") def configure_tls_mtls_wrong_client_cert(context: Context) -> None: """Configure run.yaml for mTLS with invalid client cert (CA cert as client cert).""" _configure_tls( @@ -324,7 +324,7 @@ def configure_tls_mtls_wrong_client_cert(context: Context) -> None: ) -@given("Llama Stack is configured for mTLS with untrusted client certificate") +@given("OGX is configured for mTLS with untrusted client certificate") def configure_tls_mtls_untrusted_client_cert(context: Context) -> None: """Configure run.yaml with untrusted client certificate.""" _configure_tls( @@ -338,7 +338,7 @@ def configure_tls_mtls_untrusted_client_cert(context: Context) -> None: ) -@given("Llama Stack is configured for mTLS with expired client certificate") +@given("OGX is configured for mTLS with expired client certificate") def configure_tls_mtls_expired_client_cert(context: Context) -> None: """Configure run.yaml with expired client certificate.""" _configure_tls( @@ -352,7 +352,7 @@ def configure_tls_mtls_expired_client_cert(context: Context) -> None: ) -@given("Llama Stack is configured with CA certificate and hostname mismatch server") +@given("OGX is configured with CA certificate and hostname mismatch server") def configure_tls_ca_hostname_mismatch(context: Context) -> None: """Configure run.yaml to connect to hostname-mismatch server (should fail).""" _configure_tls( @@ -361,7 +361,7 @@ def configure_tls_ca_hostname_mismatch(context: Context) -> None: ) -@given("Llama Stack is configured with mutual TLS and hostname mismatch server") +@given("OGX is configured with mutual TLS and hostname mismatch server") def configure_tls_mtls_hostname_mismatch(context: Context) -> None: """Configure run.yaml with mTLS against hostname-mismatch server.""" _configure_tls( @@ -375,14 +375,14 @@ def configure_tls_mtls_hostname_mismatch(context: Context) -> None: ) -@given('Llama Stack is configured with CA certificate path "{path}"') +@given('OGX is configured with CA certificate path "{path}"') def configure_tls_ca_path(context: Context, path: str) -> None: """Configure run.yaml with TLS verify pointing to a specific CA cert path.""" _configure_tls({"verify": path}) @given( - 'Llama Stack is configured with TLS minimum version "{version}" and CA certificate path "{path}"' + 'OGX is configured with TLS minimum version "{version}" and CA certificate path "{path}"' ) def configure_tls_min_version_and_ca(context: Context, version: str, path: str) -> None: """Configure run.yaml with TLS minimum version and a specific CA cert path.""" @@ -390,7 +390,7 @@ def configure_tls_min_version_and_ca(context: Context, version: str, path: str) @given( - 'Llama Stack is configured with TLS minimum version "{version}" and hostname mismatch server' + 'OGX is configured with TLS minimum version "{version}" and hostname mismatch server' ) def configure_tls_min_version_hostname_mismatch(context: Context, version: str) -> None: """Configure run.yaml with TLS min version against hostname-mismatch server.""" diff --git a/tests/e2e/features/tls-ca.feature b/tests/e2e/features/tls-ca.feature index 7ebb9e51a..45b1f9e37 100644 --- a/tests/e2e/features/tls-ca.feature +++ b/tests/e2e/features/tls-ca.feature @@ -1,6 +1,6 @@ @cfg_tls @skip-in-library-mode @skip-in-prow Feature: TLS configuration — CA certificate verification - Validate Llama Stack NetworkConfig.tls CA trust settings against the mock HTTPS + Validate OGX NetworkConfig.tls CA trust settings against the mock HTTPS inference provider (standard TLS port). Background: @@ -8,14 +8,14 @@ Feature: TLS configuration — CA certificate verification And The system is in default state And REST API service prefix is /v1 And the Lightspeed stack configuration directory is "tests/e2e/configuration" - And The original Llama Stack config is restored if modified + And The original OGX config is restored if modified And The mock TLS inference server is deployed And The service uses the lightspeed-stack-tls.yaml configuration And The service is restarted Scenario: Inference succeeds with TLS verification disabled - Given Llama Stack is configured with TLS verification disabled - And Llama Stack is restarted + Given OGX is configured with TLS verification disabled + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -25,8 +25,8 @@ Feature: TLS configuration — CA certificate verification And The body of the response contains Hello from the TLS mock inference server Scenario: Inference succeeds with CA certificate verification - Given Llama Stack is configured with CA certificate verification - And Llama Stack is restarted + Given OGX is configured with CA certificate verification + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -36,8 +36,8 @@ Feature: TLS configuration — CA certificate verification And The body of the response contains Hello from the TLS mock inference server Scenario: Inference fails with an untrusted CA certificate - Given Llama Stack is configured with CA certificate path "/certs/untrusted-ca.crt" - And Llama Stack is restarted + Given OGX is configured with CA certificate path "/certs/untrusted-ca.crt" + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -47,8 +47,8 @@ Feature: TLS configuration — CA certificate verification And The body of the response does not contain Hello from the TLS mock inference server Scenario: Inference fails with an expired CA certificate - Given Llama Stack is configured with CA certificate path "/certs/expired-ca.crt" - And Llama Stack is restarted + Given OGX is configured with CA certificate path "/certs/expired-ca.crt" + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -58,8 +58,8 @@ Feature: TLS configuration — CA certificate verification And The body of the response does not contain Hello from the TLS mock inference server Scenario: Inference fails when TLS verify is true against self-signed cert - Given Llama Stack is configured with TLS verification enabled - And Llama Stack is restarted + Given OGX is configured with TLS verification enabled + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -69,8 +69,8 @@ Feature: TLS configuration — CA certificate verification And The body of the response does not contain Hello from the TLS mock inference server Scenario: Inference fails with CA certificate verification and hostname mismatch - Given Llama Stack is configured with CA certificate and hostname mismatch server - And Llama Stack is restarted + Given OGX is configured with CA certificate and hostname mismatch server + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ diff --git a/tests/e2e/features/tls-mtls.feature b/tests/e2e/features/tls-mtls.feature index 053144bb6..f6eec2e70 100644 --- a/tests/e2e/features/tls-mtls.feature +++ b/tests/e2e/features/tls-mtls.feature @@ -1,6 +1,6 @@ @cfg_tls @skip-in-library-mode @skip-in-prow Feature: TLS configuration — mutual TLS authentication - Validate Llama Stack NetworkConfig.tls client certificate settings against the + Validate OGX NetworkConfig.tls client certificate settings against the mock HTTPS inference provider (mTLS port). Background: @@ -8,14 +8,14 @@ Feature: TLS configuration — mutual TLS authentication And The system is in default state And REST API service prefix is /v1 And the Lightspeed stack configuration directory is "tests/e2e/configuration" - And The original Llama Stack config is restored if modified + And The original OGX config is restored if modified And The mock TLS inference server is deployed And The service uses the lightspeed-stack-tls.yaml configuration And The service is restarted Scenario: Inference succeeds with mutual TLS authentication - Given Llama Stack is configured with mutual TLS authentication - And Llama Stack is restarted + Given OGX is configured with mutual TLS authentication + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -25,8 +25,8 @@ Feature: TLS configuration — mutual TLS authentication And The body of the response contains Hello from the TLS mock inference server Scenario: Inference fails when mTLS is required but no client certificate is provided - Given Llama Stack is configured for mTLS without client certificate - And Llama Stack is restarted + Given OGX is configured for mTLS without client certificate + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -36,8 +36,8 @@ Feature: TLS configuration — mutual TLS authentication And The body of the response does not contain Hello from the TLS mock inference server Scenario: Inference fails when mTLS is required but wrong client certificate is provided - Given Llama Stack is configured for mTLS with wrong client certificate - And Llama Stack is restarted + Given OGX is configured for mTLS with wrong client certificate + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -47,8 +47,8 @@ Feature: TLS configuration — mutual TLS authentication And The body of the response does not contain Hello from the TLS mock inference server Scenario: Inference fails when mTLS is required but untrusted client certificate is provided - Given Llama Stack is configured for mTLS with untrusted client certificate - And Llama Stack is restarted + Given OGX is configured for mTLS with untrusted client certificate + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -58,8 +58,8 @@ Feature: TLS configuration — mutual TLS authentication And The body of the response does not contain Hello from the TLS mock inference server Scenario: Inference fails when mTLS is required but expired client certificate is provided - Given Llama Stack is configured for mTLS with expired client certificate - And Llama Stack is restarted + Given OGX is configured for mTLS with expired client certificate + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -69,8 +69,8 @@ Feature: TLS configuration — mutual TLS authentication And The body of the response does not contain Hello from the TLS mock inference server Scenario: Inference fails with mutual TLS and hostname mismatch - Given Llama Stack is configured with mutual TLS and hostname mismatch server - And Llama Stack is restarted + Given OGX is configured with mutual TLS and hostname mismatch server + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ diff --git a/tests/e2e/features/tls-tlsv13.feature b/tests/e2e/features/tls-tlsv13.feature index 692b8fc1d..60a157700 100644 --- a/tests/e2e/features/tls-tlsv13.feature +++ b/tests/e2e/features/tls-tlsv13.feature @@ -1,6 +1,6 @@ @cfg_tls @skip-in-library-mode @skip-in-prow Feature: TLS configuration — TLS minimum version 1.3 - Validate Llama Stack NetworkConfig.tls min_version TLSv1.3 against the mock + Validate OGX NetworkConfig.tls min_version TLSv1.3 against the mock HTTPS inference provider. Background: @@ -8,14 +8,14 @@ Feature: TLS configuration — TLS minimum version 1.3 And The system is in default state And REST API service prefix is /v1 And the Lightspeed stack configuration directory is "tests/e2e/configuration" - And The original Llama Stack config is restored if modified + And The original OGX config is restored if modified And The mock TLS inference server is deployed And The service uses the lightspeed-stack-tls.yaml configuration And The service is restarted Scenario: Inference succeeds with TLS minimum version TLSv1.3 - Given Llama Stack is configured with TLS minimum version "TLSv1.3" and CA certificate path "/certs/ca.crt" - And Llama Stack is restarted + Given OGX is configured with TLS minimum version "TLSv1.3" and CA certificate path "/certs/ca.crt" + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -25,8 +25,8 @@ Feature: TLS configuration — TLS minimum version 1.3 And The body of the response contains Hello from the TLS mock inference server Scenario: Inference fails with TLS minimum version TLSv1.3 and untrusted CA certificate - Given Llama Stack is configured with TLS minimum version "TLSv1.3" and CA certificate path "/certs/untrusted-ca.crt" - And Llama Stack is restarted + Given OGX is configured with TLS minimum version "TLSv1.3" and CA certificate path "/certs/untrusted-ca.crt" + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -36,8 +36,8 @@ Feature: TLS configuration — TLS minimum version 1.3 And The body of the response does not contain Hello from the TLS mock inference server Scenario: Inference fails with TLS minimum version TLSv1.3 and hostname mismatch - Given Llama Stack is configured with TLS minimum version "TLSv1.3" and hostname mismatch server - And Llama Stack is restarted + Given OGX is configured with TLS minimum version "TLSv1.3" and hostname mismatch server + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -47,8 +47,8 @@ Feature: TLS configuration — TLS minimum version 1.3 And The body of the response does not contain Hello from the TLS mock inference server Scenario: Inference fails with TLS minimum version TLSv1.3 and expired CA certificate - Given Llama Stack is configured with TLS minimum version "TLSv1.3" and CA certificate path "/certs/expired-ca.crt" - And Llama Stack is restarted + Given OGX is configured with TLS minimum version "TLSv1.3" and CA certificate path "/certs/expired-ca.crt" + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ diff --git a/tests/e2e/features/unified-mode-boot.feature b/tests/e2e/features/unified-mode-boot.feature index 7c1413818..1724dd622 100644 --- a/tests/e2e/features/unified-mode-boot.feature +++ b/tests/e2e/features/unified-mode-boot.feature @@ -32,7 +32,7 @@ Feature: Unified mode configuration boot @skip-in-server-mode - Scenario: Unified config with llama_stack.config only boots and serves requests in library mode + Scenario: Unified config with ogx.config only boots and serves requests in library mode Given The service uses the lightspeed-stack-unified-config-only.yaml configuration And The service is restarted When I access endpoint "readiness" using HTTP GET method @@ -65,7 +65,7 @@ Feature: Unified mode configuration boot @skip-in-library-mode Scenario: Unified config with inference.providers boots and serves requests in server mode Given The service uses the lightspeed-stack-unified-providers.yaml configuration - And Llama Stack is restarted + And OGX is restarted And Lightspeed Stack is restarted When I access endpoint "readiness" using HTTP GET method Then The status code of the response is 200 @@ -85,9 +85,9 @@ Feature: Unified mode configuration boot @skip-in-library-mode - Scenario: Unified config with llama_stack.config only boots and serves requests in server mode + Scenario: Unified config with ogx.config only boots and serves requests in server mode Given The service uses the lightspeed-stack-unified-config-only.yaml configuration - And Llama Stack is restarted + And OGX is restarted And Lightspeed Stack is restarted When I access endpoint "readiness" using HTTP GET method Then The status code of the response is 200 @@ -101,7 +101,7 @@ Feature: Unified mode configuration boot @skip-in-library-mode Scenario: Unified config with relative profile path boots in server mode Given The service uses the lightspeed-stack-unified-relative-profile.yaml configuration - And Llama Stack is restarted + And OGX is restarted And Lightspeed Stack is restarted When I access endpoint "readiness" using HTTP GET method Then The status code of the response is 200 @@ -110,7 +110,7 @@ Feature: Unified mode configuration boot @skip-in-library-mode Scenario: Unified config with absolute profile path boots in server mode Given The service uses the lightspeed-stack-unified-absolute-profile.yaml configuration - And Llama Stack is restarted + And OGX is restarted And Lightspeed Stack is restarted When I access endpoint "readiness" using HTTP GET method Then The status code of the response is 200 diff --git a/tests/e2e/features/unified-mode-legacy.feature b/tests/e2e/features/unified-mode-legacy.feature index 407c43e6a..dea933804 100644 --- a/tests/e2e/features/unified-mode-legacy.feature +++ b/tests/e2e/features/unified-mode-legacy.feature @@ -28,7 +28,7 @@ Feature: Legacy two-file configuration during deprecation window @skip-in-library-mode Scenario: Legacy two-file configuration still boots and serves requests in server mode Given The service uses the lightspeed-stack.yaml configuration - And Llama Stack is restarted + And OGX is restarted And Lightspeed Stack is restarted When I access endpoint "readiness" using HTTP GET method Then The status code of the response is 200 diff --git a/tests/e2e/features/unified-mode-migration.feature b/tests/e2e/features/unified-mode-migration.feature index b5a2d06eb..7dbca73b1 100644 --- a/tests/e2e/features/unified-mode-migration.feature +++ b/tests/e2e/features/unified-mode-migration.feature @@ -23,7 +23,7 @@ Feature: Legacy to unified configuration migration # --- library mode (@skip-in-server-mode) --- @skip-in-server-mode - Scenario: Migrated unified configuration drives byte-identical Llama Stack behavior in library mode + Scenario: Migrated unified configuration drives byte-identical OGX behavior in library mode Given lightspeed-stack --migrate-config is run for the legacy migration fixture pair And The service uses the lightspeed-stack-unified-migrated.yaml configuration And The service is restarted @@ -39,10 +39,10 @@ Feature: Legacy to unified configuration migration # --- server mode (@skip-in-library-mode) --- @skip-in-library-mode - Scenario: Migrated unified configuration drives byte-identical Llama Stack behavior in server mode + Scenario: Migrated unified configuration drives byte-identical OGX behavior in server mode Given lightspeed-stack --migrate-config is run for the legacy migration fixture pair And The service uses the lightspeed-stack-unified-migrated.yaml configuration - And Llama Stack is restarted + And OGX is restarted And Lightspeed Stack is restarted When I access endpoint "readiness" using HTTP GET method Then The status code of the response is 200 diff --git a/tests/e2e/features/unified-mode-synthesis.feature b/tests/e2e/features/unified-mode-synthesis.feature index 52b4254fa..69f67c44d 100644 --- a/tests/e2e/features/unified-mode-synthesis.feature +++ b/tests/e2e/features/unified-mode-synthesis.feature @@ -54,6 +54,6 @@ Feature: Unified mode configuration synthesis @skip-in-library-mode Scenario: Synthesized run.yaml path is logged at startup in server mode Given The service uses the lightspeed-stack-unified-providers.yaml configuration - And Llama Stack is restarted + And OGX is restarted And Lightspeed Stack is restarted Then the lightspeed-stack container logs contain synthesized run.yaml diff --git a/tests/e2e/features/unified-mode-validation.feature b/tests/e2e/features/unified-mode-validation.feature index 5a1688335..ab7a09038 100644 --- a/tests/e2e/features/unified-mode-validation.feature +++ b/tests/e2e/features/unified-mode-validation.feature @@ -13,7 +13,7 @@ Feature: Unified mode configuration validation Then the validation error contains --migrate-config - Scenario: llama_stack.config together with library_client_config_path fails at load + Scenario: ogx.config together with library_client_config_path fails at load Given The service uses the lightspeed-stack-invalid-config-and-legacy.yaml configuration When configuration validation is attempted for the active configuration Then the validation error contains --migrate-config diff --git a/tests/e2e/utils/ogx_config_utils.py b/tests/e2e/utils/ogx_config_utils.py index 42af3b1d5..c4599c9f2 100644 --- a/tests/e2e/utils/ogx_config_utils.py +++ b/tests/e2e/utils/ogx_config_utils.py @@ -9,24 +9,24 @@ import yaml from tests.e2e.utils.ogx_prow_utils import ( - backup_llama_run_config_to_memory, - get_llama_run_config_content, - remove_llama_run_config_backup, - update_llama_run_configmap, + backup_ogx_run_config_to_memory, + get_ogx_run_config_content, + remove_ogx_run_config_backup, + update_ogx_run_configmap, ) from tests.e2e.utils.utils import is_prow_environment -_DEFAULT_LOCAL_LLAMA_CONFIG_PATH = "run.yaml" -_DEFAULT_LOCAL_LLAMA_CONFIG_BACKUP_PATH = "run.yaml.proxy-backup" -_llama_config_backup_key: dict[str, Optional[str]] = {"value": None} +_DEFAULT_LOCAL_OGX_CONFIG_PATH = "run.yaml" +_DEFAULT_LOCAL_OGX_CONFIG_BACKUP_PATH = "run.yaml.proxy-backup" +_ogx_config_backup_key: dict[str, Optional[str]] = {"value": None} -def clear_llama_config_backup() -> None: +def clear_ogx_config_backup() -> None: """Drop in-memory run.yaml backup (e.g. at start of tls.feature).""" - _llama_config_backup_key["value"] = None + _ogx_config_backup_key["value"] = None -def reset_llama_run_config_to_pipeline_default() -> None: +def reset_ogx_run_config_to_pipeline_default() -> None: """Reset llama-stack-config run.yaml to Konflux/Prow pipeline seed (run-ci.yaml).""" if not is_prow_environment(): return @@ -35,49 +35,49 @@ def reset_llama_run_config_to_pipeline_default() -> None: print(f"WARN: pipeline run.yaml seed not found at {run_ci}", flush=True) return print(f"Resetting llama-stack-config from {run_ci.name}...", flush=True) - update_llama_run_configmap(str(run_ci)) + update_ogx_run_configmap(str(run_ci)) -def _local_llama_config_path() -> str: +def _local_ogx_config_path() -> str: """Return local run.yaml path for Docker/local e2e execution.""" - return os.getenv("E2E_LLAMA_CONFIG_PATH", _DEFAULT_LOCAL_LLAMA_CONFIG_PATH) + return os.getenv("E2E_OGX_CONFIG_PATH", _DEFAULT_LOCAL_OGX_CONFIG_PATH) -def _local_llama_config_backup_path() -> str: +def _local_ogx_config_backup_path() -> str: """Return backup path used for local run.yaml mutations.""" return os.getenv( - "E2E_LLAMA_CONFIG_BACKUP_PATH", - _DEFAULT_LOCAL_LLAMA_CONFIG_BACKUP_PATH, + "E2E_OGX_CONFIG_BACKUP_PATH", + _DEFAULT_LOCAL_OGX_CONFIG_BACKUP_PATH, ) def backup_ogx_config() -> None: """Create a backup of the current OGX run config once per scenario.""" if is_prow_environment(): - if _llama_config_backup_key["value"] is None: - _llama_config_backup_key["value"] = backup_llama_run_config_to_memory() + if _ogx_config_backup_key["value"] is None: + _ogx_config_backup_key["value"] = backup_ogx_run_config_to_memory() return - backup_path = _local_llama_config_backup_path() + backup_path = _local_ogx_config_backup_path() if not os.path.exists(backup_path): - shutil.copy(_local_llama_config_path(), backup_path) + shutil.copy(_local_ogx_config_path(), backup_path) -def load_llama_config() -> dict[str, Any]: +def load_ogx_config() -> dict[str, Any]: """Load run.yaml configuration as a dictionary.""" if is_prow_environment(): - content = get_llama_run_config_content() + content = get_ogx_run_config_content() loaded = yaml.safe_load(content) or {} assert isinstance(loaded, dict), "Expected run.yaml to deserialize to a mapping" return loaded - with open(_local_llama_config_path(), encoding="utf-8") as file: + with open(_local_ogx_config_path(), encoding="utf-8") as file: loaded = yaml.safe_load(file) or {} assert isinstance(loaded, dict), "Expected run.yaml to deserialize to a mapping" return loaded -def write_llama_config(config: dict[str, Any]) -> None: +def write_ogx_config(config: dict[str, Any]) -> None: """Write run.yaml configuration in local or Prow environment.""" if is_prow_environment(): with tempfile.NamedTemporaryFile( @@ -89,33 +89,33 @@ def write_llama_config(config: dict[str, Any]) -> None: yaml.dump(config, file, default_flow_style=False) temp_path = file.name try: - update_llama_run_configmap(temp_path) + update_ogx_run_configmap(temp_path) finally: if os.path.exists(temp_path): os.remove(temp_path) return - with open(_local_llama_config_path(), "w", encoding="utf-8") as file: + with open(_local_ogx_config_path(), "w", encoding="utf-8") as file: yaml.dump(config, file, default_flow_style=False) -def restore_llama_config_if_modified() -> bool: +def restore_ogx_config_if_modified() -> bool: """Restore run config when a backup exists. Returns: True when a restore happened, otherwise False. """ if is_prow_environment(): - backup_key = _llama_config_backup_key["value"] + backup_key = _ogx_config_backup_key["value"] if backup_key is None: return False - update_llama_run_configmap(backup_key) - remove_llama_run_config_backup(backup_key) - _llama_config_backup_key["value"] = None + update_ogx_run_configmap(backup_key) + remove_ogx_run_config_backup(backup_key) + _ogx_config_backup_key["value"] = None return True - backup_path = _local_llama_config_backup_path() + backup_path = _local_ogx_config_backup_path() if not os.path.exists(backup_path): return False - shutil.move(backup_path, _local_llama_config_path()) + shutil.move(backup_path, _local_ogx_config_path()) return True diff --git a/tests/e2e/utils/ogx_prow_utils.py b/tests/e2e/utils/ogx_prow_utils.py index 57b15fe0c..f3247a845 100644 --- a/tests/e2e/utils/ogx_prow_utils.py +++ b/tests/e2e/utils/ogx_prow_utils.py @@ -7,35 +7,36 @@ update_config_configmap, ) -_LLAMA_CONFIGMAP_NAME = "llama-stack-config" -_LLAMA_CONFIGMAP_KEY = "run.yaml" +# OpenShift ConfigMap name (legacy K8s resource id in Prow manifests). +_OGX_CONFIGMAP_NAME = "llama-stack-config" +_OGX_CONFIGMAP_KEY = "run.yaml" -def get_llama_run_config_content() -> str: - """Return llama-stack-config run.yaml content in Prow/OpenShift.""" +def get_ogx_run_config_content() -> str: + """Return OGX run.yaml ConfigMap content in Prow/OpenShift.""" return get_configmap_content( - configmap_name=_LLAMA_CONFIGMAP_NAME, - configmap_key=_LLAMA_CONFIGMAP_KEY, + configmap_name=_OGX_CONFIGMAP_NAME, + configmap_key=_OGX_CONFIGMAP_KEY, ) -def backup_llama_run_config_to_memory() -> str: - """Backup llama-stack-config run.yaml into in-memory backup storage.""" +def backup_ogx_run_config_to_memory() -> str: + """Backup OGX run.yaml ConfigMap into in-memory backup storage.""" return backup_configmap_to_memory( - configmap_name=_LLAMA_CONFIGMAP_NAME, - configmap_key=_LLAMA_CONFIGMAP_KEY, + configmap_name=_OGX_CONFIGMAP_NAME, + configmap_key=_OGX_CONFIGMAP_KEY, ) -def update_llama_run_configmap(source: str) -> None: - """Update or restore llama-stack-config run.yaml from file or backup key.""" +def update_ogx_run_configmap(source: str) -> None: + """Update or restore OGX run.yaml ConfigMap from file or backup key.""" update_config_configmap( source, - configmap_name=_LLAMA_CONFIGMAP_NAME, - configmap_key=_LLAMA_CONFIGMAP_KEY, + configmap_name=_OGX_CONFIGMAP_NAME, + configmap_key=_OGX_CONFIGMAP_KEY, ) -def remove_llama_run_config_backup(backup_key: str) -> None: - """Remove a llama-stack-config run.yaml backup from in-memory storage.""" +def remove_ogx_run_config_backup(backup_key: str) -> None: + """Remove an OGX run.yaml ConfigMap backup from in-memory storage.""" remove_configmap_backup(backup_key) diff --git a/tests/e2e/utils/ogx_utils.py b/tests/e2e/utils/ogx_utils.py index 2a661ff19..5fa7d3f15 100644 --- a/tests/e2e/utils/ogx_utils.py +++ b/tests/e2e/utils/ogx_utils.py @@ -5,7 +5,7 @@ ``Given shields are disabled for this scenario`` step). Only applies when running OGX as a separate service (server mode). -Requires E2E_LLAMA_STACK_URL or E2E_LLAMA_HOSTNAME and E2E_LLAMA_PORT. +Requires E2E_OGX_STACK_URL or E2E_OGX_HOSTNAME and E2E_OGX_PORT. """ import asyncio @@ -22,16 +22,16 @@ def _get_ogx_client() -> AsyncOgxClient: """Build an AsyncOgxClient from env (for e2e test use).""" - base_url = os.getenv("E2E_LLAMA_STACK_URL") + base_url = os.getenv("E2E_OGX_STACK_URL") if not base_url: if is_prow_environment(): - host = os.getenv("E2E_LLAMA_HOSTNAME", "localhost") + host = os.getenv("E2E_OGX_HOSTNAME", "localhost") else: host = "localhost" - port = os.getenv("E2E_LLAMA_PORT", "8321") + port = os.getenv("E2E_OGX_PORT", "8321") base_url = f"http://{host}:{port}" - api_key = os.getenv("E2E_LLAMA_STACK_API_KEY", "xyzzy") - timeout = int(os.getenv("E2E_LLAMA_STACK_TIMEOUT", "60")) + api_key = os.getenv("E2E_OGX_STACK_API_KEY", "xyzzy") + timeout = int(os.getenv("E2E_OGX_STACK_TIMEOUT", "60")) return AsyncOgxClient(base_url=base_url, api_key=api_key, timeout=timeout) @@ -106,10 +106,10 @@ def register_shield( ) -> None: """Re-register the shield via client.shields.register().""" if not provider_id: - provider_id = os.getenv("E2E_LLAMA_GUARD_PROVIDER_ID", "llama-guard") + provider_id = os.getenv("E2E_OGX_GUARD_PROVIDER_ID", "llama-guard") if not provider_shield_id: provider_shield_id = os.getenv( - "E2E_LLAMA_GUARD_PROVIDER_SHIELD_ID", + "E2E_OGX_GUARD_PROVIDER_SHIELD_ID", "openai/gpt-4o-mini", ) asyncio.run(_register_shield_async(shield_id, provider_id, provider_shield_id)) diff --git a/tests/e2e/utils/prow_utils.py b/tests/e2e/utils/prow_utils.py index 9e19f0874..ea7b1bb57 100644 --- a/tests/e2e/utils/prow_utils.py +++ b/tests/e2e/utils/prow_utils.py @@ -19,7 +19,8 @@ def get_namespace() -> str: # Mapping from container names (used in tests) to pod names (used in OpenShift) _POD_NAME_MAP = { "lightspeed-stack": "lightspeed-stack-service", - "llama-stack": "llama-stack-service", + "ogx": "llama-stack-service", + "llama-stack": "llama-stack-service", # legacy alias } @@ -86,14 +87,14 @@ def wait_for_pod_health(pod_name: str, max_attempts: int = 60) -> None: raise -_LLAMA_RESTART_NAMES = frozenset({"llama-stack", "llama-stack-service"}) +_OGX_RESTART_NAMES = frozenset({"ogx", "llama-stack", "llama-stack-service"}) _LIGHTSPEED_RESTART_NAMES = frozenset({"lightspeed-stack", "lightspeed-stack-service"}) def restart_pod(container_name: str) -> None: """Restart OGX or Lightspeed pod in OpenShift/Prow (not Docker). - Maps ``container_name`` to the correct e2e-ops command: ``restart-llama-stack`` + Maps ``container_name`` to the correct e2e-ops command: ``restart-ogx`` vs ``restart-lightspeed``. Unknown names default to Lightspeed with a warning. For Lightspeed restarts, e2e-ops ensures OGX is running first. OGX pod logs @@ -102,11 +103,11 @@ def restart_pod(container_name: str) -> None: CI failures with healthy pod logs are often **localhost port-forward** contention (pipeline forward vs hook restart), not application crashes—see e2e-ops.sh header. """ - if container_name in _LLAMA_RESTART_NAMES: - op = "restart-llama-stack" + if container_name in _OGX_RESTART_NAMES: + op = "restart-ogx" # Subprocess cap must exceed e2e-ops internal waits (pod + in-pod health + port-forward). # Konflux TLS full recreate: ~6–12 min typical, 15+ min under load (user-reported 400s+). - if os.environ.get("E2E_COPY_MOCK_TLS_CERTS_TO_LLAMA") == "1": + if os.environ.get("E2E_COPY_MOCK_TLS_CERTS_TO_OGX") == "1": timeout = 1200 elif os.environ.get("E2E_KONFLUX_E2E") == "1": timeout = 720 @@ -158,24 +159,24 @@ def restore_ogx_pod() -> None: timeout = 600 else: timeout = 420 - result = run_e2e_ops("restart-llama-stack", timeout=timeout) + result = run_e2e_ops("restart-ogx", timeout=timeout) print(result.stdout, end="") if result.returncode != 0: print(result.stderr, end="") raise subprocess.CalledProcessError( - result.returncode, "restart-llama-stack", result.stderr + result.returncode, "restart-ogx", result.stderr ) print("✓ OGX pod restored successfully") -def disrupt_llama_stack_pod() -> bool: +def disrupt_ogx_pod() -> bool: """Disrupt OGX connection in Prow/OpenShift environment. Returns: True if the pod was running and has been disrupted, False otherwise. """ try: - result = run_e2e_ops("disrupt-llama-stack", timeout=90) + result = run_e2e_ops("disrupt-ogx", timeout=90) print(result.stdout, end="") # Exit code 0 = disrupted (was running), exit code 2 = was not running diff --git a/tests/e2e/utils/utils.py b/tests/e2e/utils/utils.py index 378dbde12..2fc8efd27 100644 --- a/tests/e2e/utils/utils.py +++ b/tests/e2e/utils/utils.py @@ -268,7 +268,7 @@ def wait_for_ogx_ready( ------- True if healthy; False if the wait soft-failed. """ - return wait_for_container_health("llama-stack", max_attempts=max_attempts) + return wait_for_container_health("ogx", max_attempts=max_attempts) def validate_json_partially(actual: Any, expected: Any) -> None: @@ -450,12 +450,12 @@ def restart_container(container_name: str) -> None: """ if is_prow_environment(): restart_pod(container_name) - if container_name == "llama-stack": + if container_name == "ogx": from tests.e2e.features.steps.health import ( - reset_llama_stack_disrupt_once_tracking, + reset_ogx_disrupt_once_tracking, ) - reset_llama_stack_disrupt_once_tracking() + reset_ogx_disrupt_once_tracking() return try: @@ -476,16 +476,16 @@ def restart_container(container_name: str) -> None: # that restart the container don't time out. wait_for_container_health(container_name) - if container_name == "llama-stack": + if container_name == "ogx": from tests.e2e.features.steps.health import ( - reset_llama_stack_disrupt_once_tracking, + reset_ogx_disrupt_once_tracking, ) - reset_llama_stack_disrupt_once_tracking() + reset_ogx_disrupt_once_tracking() def restart_lightspeed_stack_service( - *, wait_http: bool = False, skip_llama_restore: bool = False + *, wait_http: bool = False, skip_ogx_restore: bool = False ) -> None: """Restart the lightspeed-stack container used by Behave steps. @@ -497,22 +497,22 @@ def restart_lightspeed_stack_service( wait_http: When True, also call ``wait_for_lightspeed_stack_http_ready`` after Docker health. Default False — generic ``The service is restarted`` relies on Docker health only; proxy/tls steps opt in. - skip_llama_restore: When True on Prow/Konflux, tell e2e-ops not to + skip_ogx_restore: When True on Prow/Konflux, tell e2e-ops not to bring llama back before recreating LCS (degraded-mode startup). """ - previous = os.environ.get("E2E_SKIP_LLAMA_RESTORE_ON_LCS_RESTART") - if skip_llama_restore: - os.environ["E2E_SKIP_LLAMA_RESTORE_ON_LCS_RESTART"] = "1" + previous = os.environ.get("E2E_SKIP_OGX_RESTORE_ON_LCS_RESTART") + if skip_ogx_restore: + os.environ["E2E_SKIP_OGX_RESTORE_ON_LCS_RESTART"] = "1" try: restart_container("lightspeed-stack") if wait_http: wait_for_lightspeed_stack_http_ready() finally: - if skip_llama_restore: + if skip_ogx_restore: if previous is None: - os.environ.pop("E2E_SKIP_LLAMA_RESTORE_ON_LCS_RESTART", None) + os.environ.pop("E2E_SKIP_OGX_RESTORE_ON_LCS_RESTART", None) else: - os.environ["E2E_SKIP_LLAMA_RESTORE_ON_LCS_RESTART"] = previous + os.environ["E2E_SKIP_OGX_RESTORE_ON_LCS_RESTART"] = previous def wait_for_lightspeed_stack_http_ready( diff --git a/tests/integration/test_configuration.py b/tests/integration/test_configuration.py index 0dc5a6365..f766812f4 100644 --- a/tests/integration/test_configuration.py +++ b/tests/integration/test_configuration.py @@ -68,11 +68,11 @@ def test_loading_proper_configuration(configuration_filename: str) -> None: assert cors_config.allow_headers == ["foo_header", "bar_header", "baz_header"] # check 'ogx' section - ls_config = cfg.ogx_configuration - assert ls_config.use_as_library_client is False - assert str(ls_config.url) == "http://localhost:8321/" - assert ls_config.api_key is not None - assert ls_config.api_key.get_secret_value() == "xyzzy" + ogx_configuration = cfg.ogx_configuration + assert ogx_configuration.use_as_library_client is False + assert str(ogx_configuration.url) == "http://localhost:8321/" + assert ogx_configuration.api_key is not None + assert ogx_configuration.api_key.get_secret_value() == "xyzzy" # check 'user_data_collection' section udc_config = cfg.user_data_collection_configuration diff --git a/tests/unit/models/config/test_ogx_configuration.py b/tests/unit/models/config/test_ogx_configuration.py index 37ea25336..578961bbe 100644 --- a/tests/unit/models/config/test_ogx_configuration.py +++ b/tests/unit/models/config/test_ogx_configuration.py @@ -17,7 +17,7 @@ from utils.checks import InvalidConfigurationError # A complete, valid lightspeed-stack.yaml used as the base for root-model -# (Configuration) validation tests; individual tests override its llama_stack +# (Configuration) validation tests; individual tests override its ogx # and inference sections to exercise unified-vs-legacy mode detection. _BASE_CONFIG_PATH = "tests/configuration/lightspeed-stack.yaml" @@ -28,68 +28,68 @@ def _base_config_dict() -> dict[str, Any]: return copy.deepcopy(yaml.safe_load(file)) -def test_llama_stack_configuration_constructor(subtests: SubTests) -> None: +def test_ogx_cfg_constructor(subtests: SubTests) -> None: """ Verify that the OgxConfiguration constructor accepts valid combinations of parameters and creates instances successfully. """ with subtests.test(msg="Configuration for library mode"): - llama_stack_configuration = OgxConfiguration( + ogx_cfg = OgxConfiguration( use_as_library_client=True, library_client_config_path="tests/configuration/run.yaml", url=None, api_key=None, timeout=60, ) - assert llama_stack_configuration is not None - assert llama_stack_configuration.allow_degraded_mode is False - assert llama_stack_configuration.max_retries == constants.DEFAULT_MAX_RETRIES - assert llama_stack_configuration.retry_delay == constants.DEFAULT_RETRY_DELAY + assert ogx_cfg is not None + assert ogx_cfg.allow_degraded_mode is False + assert ogx_cfg.max_retries == constants.DEFAULT_MAX_RETRIES + assert ogx_cfg.retry_delay == constants.DEFAULT_RETRY_DELAY with subtests.test(msg="Configuration for server mode"): - llama_stack_configuration = OgxConfiguration( + ogx_cfg = OgxConfiguration( use_as_library_client=False, url=AnyHttpUrl("http://localhost"), library_client_config_path=None, api_key=None, timeout=60, ) - assert llama_stack_configuration is not None - assert llama_stack_configuration.allow_degraded_mode is False - assert llama_stack_configuration.max_retries == constants.DEFAULT_MAX_RETRIES - assert llama_stack_configuration.retry_delay == constants.DEFAULT_RETRY_DELAY + assert ogx_cfg is not None + assert ogx_cfg.allow_degraded_mode is False + assert ogx_cfg.max_retries == constants.DEFAULT_MAX_RETRIES + assert ogx_cfg.retry_delay == constants.DEFAULT_RETRY_DELAY with subtests.test(msg="Minimal configuration for server mode"): - llama_stack_configuration = OgxConfiguration( + ogx_cfg = OgxConfiguration( url="http://localhost" ) # pyright: ignore[reportCallIssue] - assert llama_stack_configuration is not None - assert llama_stack_configuration.allow_degraded_mode is False - assert llama_stack_configuration.max_retries == constants.DEFAULT_MAX_RETRIES - assert llama_stack_configuration.retry_delay == constants.DEFAULT_RETRY_DELAY + assert ogx_cfg is not None + assert ogx_cfg.allow_degraded_mode is False + assert ogx_cfg.max_retries == constants.DEFAULT_MAX_RETRIES + assert ogx_cfg.retry_delay == constants.DEFAULT_RETRY_DELAY with subtests.test(msg="Full configuration for server mode"): - llama_stack_configuration = OgxConfiguration( + ogx_cfg = OgxConfiguration( use_as_library_client=False, url="http://localhost", api_key="foo" ) # pyright: ignore[reportCallIssue] - assert llama_stack_configuration is not None - assert llama_stack_configuration.allow_degraded_mode is False - assert llama_stack_configuration.max_retries == constants.DEFAULT_MAX_RETRIES - assert llama_stack_configuration.retry_delay == constants.DEFAULT_RETRY_DELAY + assert ogx_cfg is not None + assert ogx_cfg.allow_degraded_mode is False + assert ogx_cfg.max_retries == constants.DEFAULT_MAX_RETRIES + assert ogx_cfg.retry_delay == constants.DEFAULT_RETRY_DELAY with subtests.test(msg="Degraded mode enabled"): - llama_stack_configuration = OgxConfiguration( + ogx_cfg = OgxConfiguration( url="http://localhost", allow_degraded_mode=True, ) # pyright: ignore[reportCallIssue] - assert llama_stack_configuration is not None - assert llama_stack_configuration.allow_degraded_mode is True - assert llama_stack_configuration.max_retries == constants.DEFAULT_MAX_RETRIES - assert llama_stack_configuration.retry_delay == constants.DEFAULT_RETRY_DELAY + assert ogx_cfg is not None + assert ogx_cfg.allow_degraded_mode is True + assert ogx_cfg.max_retries == constants.DEFAULT_MAX_RETRIES + assert ogx_cfg.retry_delay == constants.DEFAULT_RETRY_DELAY -def test_llama_stack_configuration_no_run_yaml() -> None: +def test_ogx_cfg_no_run_yaml() -> None: """ Verify that constructing a OgxConfiguration with a non-existent or invalid library_client_config_path raises @@ -105,7 +105,7 @@ def test_llama_stack_configuration_no_run_yaml() -> None: ) # pyright: ignore[reportCallIssue] -def test_llama_stack_wrong_configuration_constructor_no_url() -> None: +def test_ogx_wrong_configuration_constructor_no_url() -> None: """ Verify that constructing a OgxConfiguration without specifying either a URL or enabling library client mode raises @@ -118,7 +118,7 @@ def test_llama_stack_wrong_configuration_constructor_no_url() -> None: OgxConfiguration() # pyright: ignore[reportCallIssue] -def test_llama_stack_wrong_configuration_constructor_library_mode_off() -> None: +def test_ogx_wrong_configuration_constructor_library_mode_off() -> None: """Test the OgxConfiguration constructor.""" with pytest.raises( ValueError, @@ -129,7 +129,7 @@ def test_llama_stack_wrong_configuration_constructor_library_mode_off() -> None: ) # pyright: ignore[reportCallIssue] -def test_llama_stack_library_mode_without_source_is_allowed_on_nested_model() -> None: +def test_ogx_library_mode_without_source_is_allowed_on_nested_model() -> None: """The nested model no longer requires a run source in library mode. A library-mode config may be driven by the root-level inference.providers, @@ -146,7 +146,7 @@ def test_llama_stack_library_mode_without_source_is_allowed_on_nested_model() -> assert cfg.config is None -def test_llama_stack_configuration_valid_http_url() -> None: +def test_ogx_cfg_valid_http_url() -> None: """Test that valid HTTP URLs are accepted.""" config = OgxConfiguration( url="http://localhost:8321" @@ -155,55 +155,55 @@ def test_llama_stack_configuration_valid_http_url() -> None: assert str(config.url) == "http://localhost:8321/" -def test_llama_stack_configuration_valid_https_url() -> None: +def test_ogx_cfg_valid_https_url() -> None: """Test that valid HTTPS URLs are accepted.""" config = OgxConfiguration( - url="https://llama-stack.example.com:8321" + url="https://ogx.example.com:8321" ) # pyright: ignore[reportCallIssue] assert config is not None - assert str(config.url) == "https://llama-stack.example.com:8321/" + assert str(config.url) == "https://ogx.example.com:8321/" -def test_llama_stack_configuration_malformed_url_rejected() -> None: +def test_ogx_cfg_malformed_url_rejected() -> None: """Test that malformed URLs are rejected with a ValidationError.""" with pytest.raises(ValidationError, match="Input should be a valid URL"): OgxConfiguration(url="not-a-valid-url") # pyright: ignore[reportCallIssue] -def test_llama_stack_configuration_invalid_scheme_rejected() -> None: +def test_ogx_cfg_invalid_scheme_rejected() -> None: """Test that URLs without http/https scheme are rejected.""" with pytest.raises(ValidationError, match="URL scheme should be 'http' or 'https'"): OgxConfiguration(url="ftp://localhost:8321") # pyright: ignore[reportCallIssue] -def test_llama_stack_configuration_wrong_max_retries_count(subtests: SubTests) -> None: +def test_ogx_cfg_wrong_max_retries_count(subtests: SubTests) -> None: """Test that malformed URLs are rejected with a ValidationError.""" with subtests.test(msg="Configuration with zero max_retries count"): with pytest.raises(ValidationError, match="Input should be greater than 0"): OgxConfiguration( - url="https://llama-stack.example.com:8321", + url="https://ogx.example.com:8321", max_retries=0, ) # pyright: ignore[reportCallIssue] with subtests.test(msg="Configuration with negative max_retries count"): with pytest.raises(ValidationError, match="Input should be greater than 0"): OgxConfiguration( - url="https://llama-stack.example.com:8321", + url="https://ogx.example.com:8321", max_retries=-1, ) # pyright: ignore[reportCallIssue] -def test_llama_stack_configuration_wrong_retry_delay_value(subtests: SubTests) -> None: +def test_ogx_cfg_wrong_retry_delay_value(subtests: SubTests) -> None: """Test that malformed URLs are rejected with a ValidationError.""" with subtests.test(msg="Configuration with zero retry_delay value"): with pytest.raises(ValidationError, match="Input should be greater than 0"): OgxConfiguration( - url="https://llama-stack.example.com:8321", + url="https://ogx.example.com:8321", retry_delay=0, ) # pyright: ignore[reportCallIssue] with subtests.test(msg="Configuration with negative retry_delay value"): with pytest.raises(ValidationError, match="Input should be greater than 0"): OgxConfiguration( - url="https://llama-stack.example.com:8321", + url="https://ogx.example.com:8321", retry_delay=-1, ) # pyright: ignore[reportCallIssue] @@ -214,7 +214,7 @@ def test_llama_stack_configuration_wrong_retry_delay_value(subtests: SubTests) - def test_library_mode_with_unified_config_no_path_is_valid() -> None: - """Library mode driven by llama_stack.config needs no library_client_config_path.""" + """Library mode driven by ogx.config needs no library_client_config_path.""" cfg = OgxConfiguration( use_as_library_client=True, config=UnifiedOgxConfig(), @@ -236,7 +236,7 @@ def test_unified_config_accepts_byo_llm_baseline() -> None: def test_root_rejects_config_and_legacy_path_together() -> None: - """A llama_stack.config block and a legacy path in one file fail at load (R3).""" + """A ogx.config block and a legacy path in one file fail at load (R3).""" config_dict = _base_config_dict() config_dict["ogx"] = { "use_as_library_client": True, @@ -327,7 +327,7 @@ def test_root_accepts_unified_library_config() -> None: def test_root_accepts_inference_providers_only_no_config_block() -> None: """Library mode driven by inference.providers alone is valid (UX: no config:{}). - The minimal unified library config needs no llama_stack.config block — a + The minimal unified library config needs no ogx.config block — a non-empty top-level inference.providers is a sufficient synthesis input. """ config_dict = _base_config_dict() @@ -492,7 +492,7 @@ def test_root_accepts_unified_marker_with_vector_store_providers_body() -> None: def test_root_accepts_unified_marker_with_config_block_body() -> None: - """'unified' agrees with a body whose only synthesis input is llama_stack.config.""" + """'unified' agrees with a body whose only synthesis input is ogx.config.""" config_dict = _clear_synthesis_inputs(_base_config_dict()) config_dict["ogx"] = { "use_as_library_client": True, diff --git a/tests/unit/telemetry/conftest.py b/tests/unit/telemetry/conftest.py index 872f3959e..903a9170c 100644 --- a/tests/unit/telemetry/conftest.py +++ b/tests/unit/telemetry/conftest.py @@ -224,7 +224,7 @@ PII_PROVIDER_API_KEY_ENV, ] -SAMPLE_LLAMA_STACK_CONFIG: dict[str, Any] = { +SAMPLE_OGX_CONFIG: dict[str, Any] = { "version": 2, "image_name": "starter", "container_image": None, @@ -296,7 +296,7 @@ } -LLAMA_STACK_PII_VALUES = [ +OGX_PII_VALUES = [ "sk-openai-secret-key", "/secret/path/kv_store.db", "/secret/path/sql_store.db", @@ -794,7 +794,7 @@ def build_minimal_config() -> Configuration: @pytest.fixture(name="ogx_config_file") def ogx_config_file_fixture(tmp_path: Path) -> str: - """Write SAMPLE_LLAMA_STACK_CONFIG to a temp YAML file and return its path. + """Write SAMPLE_OGX_CONFIG to a temp YAML file and return its path. Parameters: ---------- @@ -805,5 +805,5 @@ def ogx_config_file_fixture(tmp_path: Path) -> str: str: Path to the temporary YAML file. """ path = tmp_path / "ogx_config.yaml" - path.write_text(yaml.dump(SAMPLE_LLAMA_STACK_CONFIG)) + path.write_text(yaml.dump(SAMPLE_OGX_CONFIG)) return str(path) diff --git a/tests/unit/telemetry/test_configuration_snapshot.py b/tests/unit/telemetry/test_configuration_snapshot.py index 57d7c5aae..1d073f7bd 100644 --- a/tests/unit/telemetry/test_configuration_snapshot.py +++ b/tests/unit/telemetry/test_configuration_snapshot.py @@ -36,9 +36,9 @@ from tests.unit.telemetry.conftest import ( ALL_PII_VALUES, BYOK_PORT, - LLAMA_STACK_PII_VALUES, + OGX_PII_VALUES, OKP_CHUNK_FILTER, - SAMPLE_LLAMA_STACK_CONFIG, + SAMPLE_OGX_CONFIG, build_fully_populated_config, build_minimal_config, ) @@ -383,20 +383,20 @@ class TestExtractStoreInfo: def test_inference_store(self) -> None: """Test inference store extraction.""" - result = _extract_store_info(SAMPLE_LLAMA_STACK_CONFIG, "inference") + result = _extract_store_info(SAMPLE_OGX_CONFIG, "inference") assert result["type"] == "sql_sqlite" assert result["db_path"] == CONFIGURED def test_metadata_store_with_namespace(self) -> None: """Test metadata store extraction includes namespace.""" - result = _extract_store_info(SAMPLE_LLAMA_STACK_CONFIG, "metadata") + result = _extract_store_info(SAMPLE_OGX_CONFIG, "metadata") assert result["type"] == "kv_sqlite" assert result["db_path"] == CONFIGURED assert result["namespace"] == "registry" def test_missing_store(self) -> None: """Test missing store returns not_configured.""" - result = _extract_store_info(SAMPLE_LLAMA_STACK_CONFIG, "nonexistent") + result = _extract_store_info(SAMPLE_OGX_CONFIG, "nonexistent") assert result["type"] == NOT_CONFIGURED assert result["db_path"] == NOT_CONFIGURED @@ -407,7 +407,7 @@ def test_no_storage_section(self) -> None: def test_db_path_is_masked(self) -> None: """Test that db_path never leaks the actual path.""" - result = _extract_store_info(SAMPLE_LLAMA_STACK_CONFIG, "inference") + result = _extract_store_info(SAMPLE_OGX_CONFIG, "inference") assert "/secret/path" not in str(result) @@ -553,43 +553,43 @@ def test_service_root_path_masked(self) -> None: snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) assert snapshot["service"]["root_path"] == CONFIGURED - def test_llama_stack_timeout_passthrough(self) -> None: - """Test llama_stack timeout passes through.""" + def test_ogx_timeout_passthrough(self) -> None: + """Test ogx timeout passes through.""" snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) assert snapshot["ogx"]["timeout"] == 180 - def test_llama_stack_max_retries_passthrough(self) -> None: - """Test llama_stack max_retries passes through.""" + def test_ogx_max_retries_passthrough(self) -> None: + """Test ogx max_retries passes through.""" snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) assert snapshot["ogx"]["max_retries"] == 5 - def test_llama_stack_retry_delay_passthrough(self) -> None: - """Test llama_stack retry_delay passes through.""" + def test_ogx_retry_delay_passthrough(self) -> None: + """Test ogx retry_delay passes through.""" snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) assert snapshot["ogx"]["retry_delay"] == 2 - def test_llama_stack_allow_degraded_mode_passthrough(self) -> None: - """Test llama_stack allow_degraded_mode passes through.""" + def test_ogx_allow_degraded_mode_passthrough(self) -> None: + """Test ogx allow_degraded_mode passes through.""" snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) assert snapshot["ogx"]["allow_degraded_mode"] is True - def test_llama_stack_config_baseline_passthrough(self) -> None: - """Test llama_stack config baseline passes through.""" + def test_ogx_config_baseline_passthrough(self) -> None: + """Test ogx config baseline passes through.""" snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) assert snapshot["ogx"]["config"]["baseline"] == "default" - def test_llama_stack_config_profile_masked(self) -> None: - """Test llama_stack config profile is masked as sensitive.""" + def test_ogx_config_profile_masked(self) -> None: + """Test ogx config profile is masked as sensitive.""" snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) assert snapshot["ogx"]["config"]["profile"] == CONFIGURED - def test_llama_stack_config_native_override_masked(self) -> None: - """Test llama_stack config native_override is masked as sensitive.""" + def test_ogx_config_native_override_masked(self) -> None: + """Test ogx config native_override is masked as sensitive.""" snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) assert snapshot["ogx"]["config"]["native_override"] == CONFIGURED - def test_llama_stack_config_none(self) -> None: - """Test llama_stack config fields when config is None.""" + def test_ogx_config_none(self) -> None: + """Test ogx config fields when config is None.""" snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) assert snapshot["ogx"]["config"]["baseline"] is None assert snapshot["ogx"]["config"]["profile"] == NOT_CONFIGURED @@ -1158,7 +1158,7 @@ class TestBuildConfigurationSnapshot: @pytest.mark.asyncio async def test_combines_both_sources(self) -> None: - """Test that snapshot contains both lightspeed_stack and llama_stack.""" + """Test that snapshot contains both lightspeed_stack and ogx.""" result = await build_configuration_snapshot(build_minimal_config(), None) assert "lightspeed_stack" in result assert "ogx" in result @@ -1197,10 +1197,10 @@ def test_no_pii_in_lightspeed_stack_snapshot(self) -> None: async def test_no_pii_in_ogx_snapshot(self, ogx_config_file: str) -> None: """Verify no PII leaks in OGX snapshot JSON.""" json_str = json.dumps(await build_ogx_snapshot(ogx_config_file)) - for pii_value in LLAMA_STACK_PII_VALUES: + for pii_value in OGX_PII_VALUES: assert ( pii_value not in json_str - ), f"PII leaked in llama-stack snapshot: '{pii_value}'" + ), f"PII leaked in OGX snapshot: '{pii_value}'" @pytest.mark.asyncio async def test_no_pii_in_combined_snapshot(self, ogx_config_file: str) -> None: @@ -1209,7 +1209,7 @@ async def test_no_pii_in_combined_snapshot(self, ogx_config_file: str) -> None: build_fully_populated_config(), ogx_config_file ) json_str = json.dumps(snapshot) - for pii_value in ALL_PII_VALUES + LLAMA_STACK_PII_VALUES: + for pii_value in ALL_PII_VALUES + OGX_PII_VALUES: assert ( pii_value not in json_str ), f"PII leaked in combined snapshot: '{pii_value}'" diff --git a/tests/unit/test_lightspeed_stack.py b/tests/unit/test_lightspeed_stack.py index e2da56084..59931b587 100644 --- a/tests/unit/test_lightspeed_stack.py +++ b/tests/unit/test_lightspeed_stack.py @@ -168,7 +168,7 @@ def test_main_does_not_warn_in_unified_mode( monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: - """A unified-mode config (llama_stack.config) emits no deprecation WARN.""" + """A unified-mode config (ogx.config) emits no deprecation WARN.""" config_yaml = COMMON_CONFIG_SECTIONS + """ ogx: use_as_library_client: true diff --git a/tests/unit/test_ogx_configuration.py b/tests/unit/test_ogx_configuration.py index 19be9278f..a8d9b5035 100644 --- a/tests/unit/test_ogx_configuration.py +++ b/tests/unit/test_ogx_configuration.py @@ -36,7 +36,7 @@ def test_enrich_azure_entra_id_inference_skips_when_not_configured() -> None: """Test enrich_azure_entra_id_inference does nothing without Entra ID config.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": { "inference": [ { @@ -47,15 +47,15 @@ def test_enrich_azure_entra_id_inference_skips_when_not_configured() -> None: ] } } - enrich_azure_entra_id_inference(ls_config, None) - assert ls_config["providers"]["inference"][0]["config"] == { + enrich_azure_entra_id_inference(ogx_config, None) + assert ogx_config["providers"]["inference"][0]["config"] == { "model_validation": True } def test_enrich_azure_entra_id_inference_sets_model_validation_false() -> None: """Test enrich_azure_entra_id_inference disables startup model validation.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": { "inference": [ { @@ -66,8 +66,8 @@ def test_enrich_azure_entra_id_inference_sets_model_validation_false() -> None: ] } } - enrich_azure_entra_id_inference(ls_config, {"tenant_id": "t"}) - azure_config = ls_config["providers"]["inference"][0]["config"] + enrich_azure_entra_id_inference(ogx_config, {"tenant_id": "t"}) + azure_config = ogx_config["providers"]["inference"][0]["config"] assert azure_config["model_validation"] is False @@ -115,15 +115,15 @@ def test_generate_configuration_enriches_azure_entra_id(tmp_path: Path) -> None: def test_construct_vector_stores_section_empty() -> None: """Test with no BYOK RAG config.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag: list[dict[str, Any]] = [] - output = construct_vector_stores_section(ls_config, byok_rag) + output = construct_vector_stores_section(ogx_config, byok_rag) assert len(output) == 0 def test_construct_vector_stores_section_preserves_existing() -> None: """Test preserves existing vector_stores entries.""" - ls_config = { + ogx_config = { "registered_resources": { "vector_stores": [ {"vector_store_id": "existing", "provider_id": "existing_provider"}, @@ -131,14 +131,14 @@ def test_construct_vector_stores_section_preserves_existing() -> None: } } byok_rag: list[dict[str, Any]] = [] - output = construct_vector_stores_section(ls_config, byok_rag) + output = construct_vector_stores_section(ogx_config, byok_rag) assert len(output) == 1 assert output[0]["vector_store_id"] == "existing" def test_construct_vector_stores_section_adds_new() -> None: """Test adds new BYOK RAG entries.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [ { "rag_id": "rag1", @@ -147,7 +147,7 @@ def test_construct_vector_stores_section_adds_new() -> None: "embedding_dimension": 512, }, ] - output = construct_vector_stores_section(ls_config, byok_rag) + output = construct_vector_stores_section(ogx_config, byok_rag) assert len(output) == 1 assert output[0]["vector_store_id"] == "store1" assert output[0]["provider_id"] == "byok_rag1" @@ -157,17 +157,17 @@ def test_construct_vector_stores_section_adds_new() -> None: def test_construct_vector_stores_section_merge() -> None: """Test merges existing and new entries.""" - ls_config = { + ogx_config = { "registered_resources": {"vector_stores": [{"vector_store_id": "existing"}]} } byok_rag = [{"rag_id": "rag1", "vector_db_id": "new_store"}] - output = construct_vector_stores_section(ls_config, byok_rag) + output = construct_vector_stores_section(ogx_config, byok_rag) assert len(output) == 2 def test_construct_vector_stores_section_skips_duplicate_from_existing() -> None: """Test skips BYOK entry when vector_store_id already exists in config.""" - ls_config = { + ogx_config = { "registered_resources": { "vector_stores": [ {"vector_store_id": "store1", "provider_id": "original_provider"}, @@ -182,7 +182,7 @@ def test_construct_vector_stores_section_skips_duplicate_from_existing() -> None "embedding_dimension": 512, }, ] - output = construct_vector_stores_section(ls_config, byok_rag) + output = construct_vector_stores_section(ogx_config, byok_rag) assert len(output) == 1 assert output[0]["provider_id"] == "original_provider" @@ -192,7 +192,7 @@ def test_construct_vector_stores_section_skips_duplicate_env_var( ) -> None: """Test skips BYOK entry when existing store uses an env var that resolves to the same ID.""" monkeypatch.setenv("FAISS_VECTOR_STORE_ID", "vs_abc123") - ls_config = { + ogx_config = { "registered_resources": { "vector_stores": [ { @@ -210,14 +210,14 @@ def test_construct_vector_stores_section_skips_duplicate_env_var( "embedding_dimension": 768, }, ] - output = construct_vector_stores_section(ls_config, byok_rag) + output = construct_vector_stores_section(ogx_config, byok_rag) assert len(output) == 1 assert output[0]["provider_id"] == "faiss" def test_construct_vector_stores_section_skips_duplicate_within_byok() -> None: """Test skips duplicate vector_db_id entries within the BYOK RAG list.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [ { "rag_id": "rag1", @@ -232,7 +232,7 @@ def test_construct_vector_stores_section_skips_duplicate_within_byok() -> None: "embedding_dimension": 768, }, ] - output = construct_vector_stores_section(ls_config, byok_rag) + output = construct_vector_stores_section(ogx_config, byok_rag) assert len(output) == 1 assert output[0]["embedding_model"] == "sentence-transformers/byok_rag1_embedding" @@ -244,24 +244,24 @@ def test_construct_vector_stores_section_skips_duplicate_within_byok() -> None: def test_construct_vector_io_providers_section_empty() -> None: """Test with no BYOK RAG config.""" - ls_config: dict[str, Any] = {"providers": {}} + ogx_config: dict[str, Any] = {"providers": {}} byok_rag: list[dict[str, Any]] = [] - output = construct_vector_io_providers_section(ls_config, byok_rag) + output = construct_vector_io_providers_section(ogx_config, byok_rag) assert len(output) == 0 def test_construct_vector_io_providers_section_preserves_existing() -> None: """Test preserves existing vector_io entries.""" - ls_config = {"providers": {"vector_io": [{"provider_id": "existing"}]}} + ogx_config = {"providers": {"vector_io": [{"provider_id": "existing"}]}} byok_rag: list[dict[str, Any]] = [] - output = construct_vector_io_providers_section(ls_config, byok_rag) + output = construct_vector_io_providers_section(ogx_config, byok_rag) assert len(output) == 1 assert output[0]["provider_id"] == "existing" def test_construct_vector_io_providers_section_adds_new() -> None: """Test adds new BYOK RAG entries using rag_id for provider naming.""" - ls_config: dict[str, Any] = {"providers": {}} + ogx_config: dict[str, Any] = {"providers": {}} byok_rag = [ { "rag_id": "rag1", @@ -269,7 +269,7 @@ def test_construct_vector_io_providers_section_adds_new() -> None: "backend": "faiss", }, ] - output = construct_vector_io_providers_section(ls_config, byok_rag) + output = construct_vector_io_providers_section(ogx_config, byok_rag) assert len(output) == 1 assert output[0]["provider_id"] == "byok_rag1" assert output[0]["provider_type"] == "inline::faiss" @@ -286,10 +286,10 @@ def test_construct_vector_io_providers_section_idempotent_reenrichment() -> None "backend": "faiss", }, ] - ls_config: dict[str, Any] = {"providers": {}} - first = construct_vector_io_providers_section(ls_config, byok_rag) - ls_config["providers"] = {"vector_io": first} - second = construct_vector_io_providers_section(ls_config, byok_rag) + ogx_config: dict[str, Any] = {"providers": {}} + first = construct_vector_io_providers_section(ogx_config, byok_rag) + ogx_config["providers"] = {"vector_io": first} + second = construct_vector_io_providers_section(ogx_config, byok_rag) assert len(second) == 1 assert second[0]["provider_id"] == "byok_rag1" @@ -306,7 +306,7 @@ def test_construct_vector_io_providers_section_collapses_existing_duplicates() - } }, } - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": {"vector_io": [dup, dup, dup]}, } byok_rag = [ @@ -316,14 +316,14 @@ def test_construct_vector_io_providers_section_collapses_existing_duplicates() - "backend": "faiss", }, ] - output = construct_vector_io_providers_section(ls_config, byok_rag) + output = construct_vector_io_providers_section(ogx_config, byok_rag) assert len(output) == 1 assert output[0]["provider_id"] == "byok_rag1" def test_construct_vector_io_providers_section_pgvector() -> None: """Test generates correct pgvector provider config.""" - ls_config: dict[str, Any] = {"providers": {}} + ogx_config: dict[str, Any] = {"providers": {}} byok_rag = [ { "rag_id": "pg1", @@ -336,7 +336,7 @@ def test_construct_vector_io_providers_section_pgvector() -> None: "password": "${env.POSTGRES_PASSWORD}", }, ] - output = construct_vector_io_providers_section(ls_config, byok_rag) + output = construct_vector_io_providers_section(ogx_config, byok_rag) assert len(output) == 1 provider = output[0] assert provider["provider_id"] == "byok_pg1" @@ -349,7 +349,7 @@ def test_construct_vector_io_providers_section_pgvector() -> None: def test_construct_vector_io_providers_section_mixed() -> None: """Test mixed faiss and pgvector entries generate correct configs.""" - ls_config: dict[str, Any] = {"providers": {}} + ogx_config: dict[str, Any] = {"providers": {}} byok_rag = [ {"rag_id": "f1", "vector_db_id": "vs_f", "backend": "faiss"}, { @@ -363,7 +363,7 @@ def test_construct_vector_io_providers_section_mixed() -> None: "password": "pass", }, ] - output = construct_vector_io_providers_section(ls_config, byok_rag) + output = construct_vector_io_providers_section(ogx_config, byok_rag) assert len(output) == 2 faiss_p = next(p for p in output if p["provider_id"] == "byok_f1") assert faiss_p["provider_type"] == "inline::faiss" @@ -376,20 +376,20 @@ def test_construct_vector_io_providers_section_mixed() -> None: def test_construct_storage_backends_section_skips_pgvector() -> None: """Test pgvector entries are skipped (they use kv_default).""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [{"rag_id": "pg1", "vector_db_id": "vs_pg", "backend": "pgvector"}] - output = construct_storage_backends_section(ls_config, byok_rag) + output = construct_storage_backends_section(ogx_config, byok_rag) assert len(output) == 0 def test_construct_storage_backends_section_mixed_faiss_pgvector() -> None: """Test only faiss entries get storage backends, pgvector is skipped.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [ {"rag_id": "f1", "vector_db_id": "vs_f", "db_path": "/tmp/f.db"}, {"rag_id": "pg1", "vector_db_id": "vs_pg", "backend": "pgvector"}, ] - output = construct_storage_backends_section(ls_config, byok_rag) + output = construct_storage_backends_section(ogx_config, byok_rag) assert len(output) == 1 assert "byok_f1_storage" in output assert "byok_pg1_storage" not in output @@ -397,7 +397,7 @@ def test_construct_storage_backends_section_mixed_faiss_pgvector() -> None: def test_enrich_byok_rag_pgvector_end_to_end() -> None: """Test enrich_byok_rag with a pgvector store entry.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [ { "rag_id": "pg1", @@ -412,15 +412,16 @@ def test_enrich_byok_rag_pgvector_end_to_end() -> None: "password": "${env.POSTGRES_PASSWORD}", }, ] - enrich_byok_rag(ls_config, byok_rag) - assert "byok_pg1_storage" not in ls_config.get("storage", {}).get("backends", {}) - providers = ls_config["providers"]["vector_io"] + enrich_byok_rag(ogx_config, byok_rag) + assert "byok_pg1_storage" not in ogx_config.get("storage", {}).get("backends", {}) + providers = ogx_config["providers"]["vector_io"] pg_p = next(p for p in providers if p["provider_id"] == "byok_pg1") assert pg_p["provider_type"] == "remote::pgvector" assert pg_p["config"]["persistence"]["backend"] == "kv_default" assert pg_p["config"]["host"] == "${env.POSTGRES_HOST}" store_ids = [ - s["vector_store_id"] for s in ls_config["registered_resources"]["vector_stores"] + s["vector_store_id"] + for s in ogx_config["registered_resources"]["vector_stores"] ] assert "vs_pg" in store_ids @@ -432,19 +433,19 @@ def test_enrich_byok_rag_skipped_still_dedupes_vector_io() -> None: "provider_type": "inline::faiss", "config": {"persistence": {"namespace": "vector_io::faiss", "backend": "b"}}, } - ls_config: dict[str, Any] = {"providers": {"vector_io": [dup, dup, dup]}} - enrich_byok_rag(ls_config, []) - assert len(ls_config["providers"]["vector_io"]) == 1 + ogx_config: dict[str, Any] = {"providers": {"vector_io": [dup, dup, dup]}} + enrich_byok_rag(ogx_config, []) + assert len(ogx_config["providers"]["vector_io"]) == 1 def test_dedupe_providers_vector_io_in_place() -> None: """dedupe_providers_vector_io keeps one entry per provider_id.""" a = {"provider_id": "p1", "provider_type": "inline::faiss", "config": {}} - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": {"vector_io": [a, a, {"provider_id": "p2"}]} } - dedupe_providers_vector_io(ls_config) - ids = [p["provider_id"] for p in ls_config["providers"]["vector_io"]] + dedupe_providers_vector_io(ogx_config) + ids = [p["provider_id"] for p in ogx_config["providers"]["vector_io"]] assert ids == ["p1", "p2"] @@ -455,15 +456,15 @@ def test_dedupe_providers_vector_io_in_place() -> None: def test_construct_storage_backends_section_empty() -> None: """Test with no BYOK RAG config.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag: list[dict[str, Any]] = [] - output = construct_storage_backends_section(ls_config, byok_rag) + output = construct_storage_backends_section(ogx_config, byok_rag) assert len(output) == 0 def test_construct_storage_backends_section_preserves_existing() -> None: """Test preserves existing backends.""" - ls_config = { + ogx_config = { "storage": { "backends": { "kv_default": {"type": "kv_sqlite", "db_path": "~/.llama/kv.db"} @@ -471,14 +472,14 @@ def test_construct_storage_backends_section_preserves_existing() -> None: } } byok_rag: list[dict[str, Any]] = [] - output = construct_storage_backends_section(ls_config, byok_rag) + output = construct_storage_backends_section(ogx_config, byok_rag) assert len(output) == 1 assert "kv_default" in output def test_construct_storage_backends_section_adds_new() -> None: """Test adds new BYOK RAG backend entries using rag_id for backend naming.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [ { "rag_id": "rag1", @@ -486,7 +487,7 @@ def test_construct_storage_backends_section_adds_new() -> None: "db_path": "/path/to/store1.db", }, ] - output = construct_storage_backends_section(ls_config, byok_rag) + output = construct_storage_backends_section(ogx_config, byok_rag) assert len(output) == 1 assert "byok_rag1_storage" in output assert output["byok_rag1_storage"]["type"] == "kv_sqlite" @@ -500,28 +501,28 @@ def test_construct_storage_backends_section_adds_new() -> None: def test_construct_models_section_empty() -> None: """Test with no BYOK RAG config.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag: list[dict[str, Any]] = [] - output = construct_models_section(ls_config, byok_rag) + output = construct_models_section(ogx_config, byok_rag) assert len(output) == 0 def test_construct_models_section_preserves_existing() -> None: """Test preserves existing models.""" - ls_config = { + ogx_config = { "registered_resources": { "models": [{"model_id": "existing", "model_type": "llm"}] } } byok_rag: list[dict[str, Any]] = [] - output = construct_models_section(ls_config, byok_rag) + output = construct_models_section(ogx_config, byok_rag) assert len(output) == 1 assert output[0]["model_id"] == "existing" def test_construct_models_section_adds_embedding_model() -> None: """Test adds embedding model from BYOK RAG using rag_id for model naming.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [ { "rag_id": "rag1", @@ -530,7 +531,7 @@ def test_construct_models_section_adds_embedding_model() -> None: "embedding_dimension": 768, }, ] - output = construct_models_section(ls_config, byok_rag) + output = construct_models_section(ogx_config, byok_rag) assert len(output) == 1 assert output[0]["model_id"] == "byok_rag1_embedding" assert output[0]["model_type"] == "embedding" @@ -541,7 +542,7 @@ def test_construct_models_section_adds_embedding_model() -> None: def test_construct_models_section_strips_prefix() -> None: """Test strips sentence-transformers/ prefix from embedding model.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [ { "rag_id": "rag1", @@ -550,14 +551,14 @@ def test_construct_models_section_strips_prefix() -> None: "embedding_dimension": 768, }, ] - output = construct_models_section(ls_config, byok_rag) + output = construct_models_section(ogx_config, byok_rag) assert len(output) == 1 assert output[0]["provider_model_id"] == "/usr/path/model" def test_byok_vector_store_uses_registered_embedding_id_not_load_path() -> None: """BYOK store lookup id matches registered model; path stays on provider_model_id.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [ { "rag_id": "rhdh-docs", @@ -566,8 +567,8 @@ def test_byok_vector_store_uses_registered_embedding_id_not_load_path() -> None: "embedding_dimension": 768, }, ] - stores = construct_vector_stores_section(ls_config, byok_rag) - models = construct_models_section(ls_config, byok_rag) + stores = construct_vector_stores_section(ogx_config, byok_rag) + models = construct_models_section(ogx_config, byok_rag) assert stores[0]["embedding_model"] == ( "sentence-transformers/byok_rhdh-docs_embedding" ) @@ -577,7 +578,7 @@ def test_byok_vector_store_uses_registered_embedding_id_not_load_path() -> None: def test_construct_models_section_registers_alias_per_rag_id_for_shared_path() -> None: """Two BYOK entries sharing a load path each get a byok__embedding alias.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [ { "rag_id": "docs-a", @@ -592,8 +593,8 @@ def test_construct_models_section_registers_alias_per_rag_id_for_shared_path() - "embedding_dimension": 768, }, ] - models = construct_models_section(ls_config, byok_rag) - stores = construct_vector_stores_section(ls_config, byok_rag) + models = construct_models_section(ogx_config, byok_rag) + stores = construct_vector_stores_section(ogx_config, byok_rag) assert {m["model_id"] for m in models} == { "byok_docs-a_embedding", "byok_docs-b_embedding", @@ -609,42 +610,42 @@ def test_construct_models_section_registers_alias_per_rag_id_for_shared_path() - def test_construct_storage_backends_section_raises_on_missing_rag_id() -> None: """Test raises ValueError when rag_id is missing from a BYOK RAG entry.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [{"vector_db_id": "store1"}] with pytest.raises(ValueError, match="missing required 'rag_id'"): - construct_storage_backends_section(ls_config, byok_rag) + construct_storage_backends_section(ogx_config, byok_rag) def test_construct_vector_stores_section_raises_on_missing_rag_id() -> None: """Test raises ValueError when rag_id is missing from a BYOK RAG entry.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [{"vector_db_id": "store1"}] with pytest.raises(ValueError, match="missing required 'rag_id'"): - construct_vector_stores_section(ls_config, byok_rag) + construct_vector_stores_section(ogx_config, byok_rag) def test_construct_vector_stores_section_raises_on_missing_vector_db_id() -> None: """Test raises ValueError when vector_db_id is missing from a BYOK RAG entry.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [{"rag_id": "rag1"}] with pytest.raises(ValueError, match="missing required 'vector_db_id'"): - construct_vector_stores_section(ls_config, byok_rag) + construct_vector_stores_section(ogx_config, byok_rag) def test_construct_vector_io_section_raises_on_missing_rag_id() -> None: """Test raises ValueError when rag_id is missing from a BYOK RAG entry.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [{"vector_db_id": "store1"}] with pytest.raises(ValueError, match="missing required 'rag_id'"): - construct_vector_io_providers_section(ls_config, byok_rag) + construct_vector_io_providers_section(ogx_config, byok_rag) def test_construct_models_section_raises_on_missing_rag_id() -> None: """Test raises ValueError when rag_id is missing from a BYOK RAG entry.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [{"vector_db_id": "store1", "embedding_model": "some-model"}] with pytest.raises(ValueError, match="missing required 'rag_id'"): - construct_models_section(ls_config, byok_rag) + construct_models_section(ogx_config, byok_rag) # ============================================================================= @@ -825,91 +826,94 @@ def test_generate_configuration_with_pgvector(tmp_path: Path) -> None: def test_enrich_solr_skips_when_not_enabled() -> None: """Test enrich_solr does nothing when OKP is not in rag inline or tool lists.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, {"inline": [], "tool": []}, {}) - assert not ls_config + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, {"inline": [], "tool": []}, {}) + assert not ogx_config def test_enrich_solr_skips_when_empty_config() -> None: """Test enrich_solr does nothing with empty rag config.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, {}, {}) - assert not ls_config + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, {}, {}) + assert not ogx_config def test_enrich_solr_adds_vector_io_provider() -> None: """Test enrich_solr adds Solr provider to vector_io section.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, _OKP_RAG_CONFIG, {}) + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {}) - assert "providers" in ls_config - assert "vector_io" in ls_config["providers"] - provider_ids = [p["provider_id"] for p in ls_config["providers"]["vector_io"]] + assert "providers" in ogx_config + assert "vector_io" in ogx_config["providers"] + provider_ids = [p["provider_id"] for p in ogx_config["providers"]["vector_io"]] assert "okp_solr" in provider_ids def test_enrich_solr_adds_vector_store_registration() -> None: """Test enrich_solr registers the Solr vector store.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, _OKP_RAG_CONFIG, {}) + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {}) - assert "registered_resources" in ls_config + assert "registered_resources" in ogx_config store_ids = [ - s["vector_store_id"] for s in ls_config["registered_resources"]["vector_stores"] + s["vector_store_id"] + for s in ogx_config["registered_resources"]["vector_stores"] ] assert "portal-rag" in store_ids def test_enrich_solr_adds_embedding_model() -> None: """Test enrich_solr registers the Solr embedding model.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, _OKP_RAG_CONFIG, {}) + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {}) - model_ids = [m["model_id"] for m in ls_config["registered_resources"]["models"]] + model_ids = [m["model_id"] for m in ogx_config["registered_resources"]["models"]] assert "sentence-transformers/solr_embedding" in model_ids def test_enrich_solr_skips_duplicate_provider() -> None: """Test enrich_solr does not add duplicate Solr provider.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": {"vector_io": [{"provider_id": "okp_solr"}]} } - enrich_solr(ls_config, _OKP_RAG_CONFIG, {}) + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {}) - provider_ids = [p["provider_id"] for p in ls_config["providers"]["vector_io"]] + provider_ids = [p["provider_id"] for p in ogx_config["providers"]["vector_io"]] assert provider_ids.count("okp_solr") == 1 def test_enrich_solr_skips_duplicate_vector_store() -> None: """Test enrich_solr does not add duplicate vector store registration.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "registered_resources": {"vector_stores": [{"vector_store_id": "portal-rag"}]} } - enrich_solr(ls_config, _OKP_RAG_CONFIG, {}) + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {}) store_ids = [ - s["vector_store_id"] for s in ls_config["registered_resources"]["vector_stores"] + s["vector_store_id"] + for s in ogx_config["registered_resources"]["vector_stores"] ] assert store_ids.count("portal-rag") == 1 def test_enrich_solr_preserves_existing_config() -> None: """Test enrich_solr preserves existing providers and resources.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": {"vector_io": [{"provider_id": "existing_provider"}]}, "registered_resources": { "vector_stores": [{"vector_store_id": "existing_store"}], "models": [{"model_id": "existing_model"}], }, } - enrich_solr(ls_config, _OKP_RAG_CONFIG, {}) + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {}) - provider_ids = [p["provider_id"] for p in ls_config["providers"]["vector_io"]] + provider_ids = [p["provider_id"] for p in ogx_config["providers"]["vector_io"]] assert "existing_provider" in provider_ids assert "okp_solr" in provider_ids store_ids = [ - s["vector_store_id"] for s in ls_config["registered_resources"]["vector_stores"] + s["vector_store_id"] + for s in ogx_config["registered_resources"]["vector_stores"] ] assert "existing_store" in store_ids assert "portal-rag" in store_ids @@ -917,11 +921,13 @@ def test_enrich_solr_preserves_existing_config() -> None: def test_enrich_solr_default_chunk_filter_query() -> None: """Test enrich_solr uses the internal chunk filter when no user filter is set.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, _OKP_RAG_CONFIG, {}) + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {}) provider = next( - p for p in ls_config["providers"]["vector_io"] if p["provider_id"] == "okp_solr" + p + for p in ogx_config["providers"]["vector_io"] + if p["provider_id"] == "okp_solr" ) assert ( provider["config"]["chunk_window_config"]["chunk_filter_query"] @@ -931,11 +937,13 @@ def test_enrich_solr_default_chunk_filter_query() -> None: def test_enrich_solr_user_chunk_filter_query_is_conjoined() -> None: """Test enrich_solr ANDs the user filter with the internal chunk filter.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, _OKP_RAG_CONFIG, {"chunk_filter_query": "product:ansible"}) + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {"chunk_filter_query": "product:ansible"}) provider = next( - p for p in ls_config["providers"]["vector_io"] if p["provider_id"] == "okp_solr" + p + for p in ogx_config["providers"]["vector_io"] + if p["provider_id"] == "okp_solr" ) assert provider["config"]["chunk_window_config"]["chunk_filter_query"] == ( "is_chunk:true AND product:ansible" @@ -944,64 +952,64 @@ def test_enrich_solr_user_chunk_filter_query_is_conjoined() -> None: def test_enrich_solr_sets_default_search_mode_keyword() -> None: """Test enrich_solr propagates search_mode keyword to vector_stores config.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, _OKP_RAG_CONFIG, {"search_mode": "keyword"}) + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {"search_mode": "keyword"}) assert ( - ls_config["vector_stores"]["chunk_retrieval_params"]["default_search_mode"] + ogx_config["vector_stores"]["chunk_retrieval_params"]["default_search_mode"] == "keyword" ) def test_enrich_solr_sets_default_search_mode_hybrid() -> None: """Test enrich_solr propagates search_mode hybrid to vector_stores config.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, _OKP_RAG_CONFIG, {"search_mode": "hybrid"}) + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {"search_mode": "hybrid"}) assert ( - ls_config["vector_stores"]["chunk_retrieval_params"]["default_search_mode"] + ogx_config["vector_stores"]["chunk_retrieval_params"]["default_search_mode"] == "hybrid" ) def test_enrich_solr_maps_semantic_to_vector() -> None: """Test enrich_solr maps LCORE semantic to OGX vector search mode.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, _OKP_RAG_CONFIG, {"search_mode": "semantic"}) + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {"search_mode": "semantic"}) assert ( - ls_config["vector_stores"]["chunk_retrieval_params"]["default_search_mode"] + ogx_config["vector_stores"]["chunk_retrieval_params"]["default_search_mode"] == "vector" ) def test_enrich_solr_maps_lexical_to_keyword() -> None: """Test enrich_solr maps LCORE lexical to OGX keyword via SOLR_SEARCH_MODE_MAP.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, _OKP_RAG_CONFIG, {"search_mode": "lexical"}) + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {"search_mode": "lexical"}) assert ( - ls_config["vector_stores"]["chunk_retrieval_params"]["default_search_mode"] + ogx_config["vector_stores"]["chunk_retrieval_params"]["default_search_mode"] == "keyword" ) def test_enrich_solr_no_search_mode_skips_vector_stores() -> None: """Test enrich_solr does not set vector_stores when search_mode is absent.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, _OKP_RAG_CONFIG, {}) + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {}) - assert "vector_stores" not in ls_config + assert "vector_stores" not in ogx_config def test_enrich_solr_preserves_existing_vector_stores() -> None: """Test enrich_solr preserves existing vector_stores config when adding search_mode.""" - ls_config: dict[str, Any] = {"vector_stores": {"default_provider_id": "faiss"}} - enrich_solr(ls_config, _OKP_RAG_CONFIG, {"search_mode": "keyword"}) + ogx_config: dict[str, Any] = {"vector_stores": {"default_provider_id": "faiss"}} + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {"search_mode": "keyword"}) - assert ls_config["vector_stores"]["default_provider_id"] == "faiss" + assert ogx_config["vector_stores"]["default_provider_id"] == "faiss" assert ( - ls_config["vector_stores"]["chunk_retrieval_params"]["default_search_mode"] + ogx_config["vector_stores"]["chunk_retrieval_params"]["default_search_mode"] == "keyword" ) @@ -1013,7 +1021,7 @@ def test_enrich_solr_preserves_existing_vector_stores() -> None: def test_enrich_vector_store_faiss_appends() -> None: """Faiss provider appends vector_io, backend, and default_* settings.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": { "vector_io": [ { @@ -1038,7 +1046,7 @@ def test_enrich_vector_store_faiss_appends() -> None: }, } enrich_vector_store( - ls_config, + ogx_config, { "default_provider": "notebooks", "providers": [ @@ -1052,26 +1060,26 @@ def test_enrich_vector_store_faiss_appends() -> None: ], }, ) - ids = {p["provider_id"] for p in ls_config["providers"]["vector_io"]} + ids = {p["provider_id"] for p in ogx_config["providers"]["vector_io"]} assert ids == {"faiss", "notebooks"} assert ( - ls_config["storage"]["backends"]["vsprov_notebooks_storage"]["db_path"] + ogx_config["storage"]["backends"]["vsprov_notebooks_storage"]["db_path"] == "/var/lib/notebooks.db" ) - assert ls_config["vector_stores"]["default_provider_id"] == "notebooks" - assert ls_config["vector_stores"]["default_embedding_model"]["model_id"] == ( + assert ogx_config["vector_stores"]["default_provider_id"] == "notebooks" + assert ogx_config["vector_stores"]["default_embedding_model"]["model_id"] == ( "vsprov_notebooks_embedding" ) assert ( - ls_config["vector_stores"]["annotation_prompt_params"]["enable_annotations"] + ogx_config["vector_stores"]["annotation_prompt_params"]["enable_annotations"] is False ) - assert not ls_config["registered_resources"]["vector_stores"] + assert not ogx_config["registered_resources"]["vector_stores"] def test_enrich_vector_store_replaces_same_provider_id() -> None: """Same provider_id replaces the baseline entry and leaves orphan backends.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": { "vector_io": [ { @@ -1098,7 +1106,7 @@ def test_enrich_vector_store_replaces_same_provider_id() -> None: "vector_stores": {}, } enrich_vector_store( - ls_config, + ogx_config, { "default_provider": "notebooks", "providers": [ @@ -1112,29 +1120,29 @@ def test_enrich_vector_store_replaces_same_provider_id() -> None: ], }, ) - providers = ls_config["providers"]["vector_io"] + providers = ogx_config["providers"]["vector_io"] assert len(providers) == 1 assert providers[0]["provider_id"] == "notebooks" assert ( providers[0]["config"]["persistence"]["backend"] == "vsprov_notebooks_storage" ) - assert "kv_notebooks" in ls_config["storage"]["backends"] + assert "kv_notebooks" in ogx_config["storage"]["backends"] assert ( - ls_config["storage"]["backends"]["vsprov_notebooks_storage"]["db_path"] + ogx_config["storage"]["backends"]["vsprov_notebooks_storage"]["db_path"] == "/new/notebooks.db" ) def test_enrich_vector_store_pgvector_no_kv_backend() -> None: """Pgvector provider does not create a kv_sqlite storage backend.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": {}, "storage": {"backends": {}}, "registered_resources": {}, "vector_stores": {}, } enrich_vector_store( - ls_config, + ogx_config, { "default_provider": "nb-pg", "providers": [ @@ -1154,7 +1162,7 @@ def test_enrich_vector_store_pgvector_no_kv_backend() -> None: ], }, ) - provider = ls_config["providers"]["vector_io"][0] + provider = ogx_config["providers"]["vector_io"][0] assert provider["provider_type"] == "remote::pgvector" assert provider["config"]["persistence"]["backend"] == "kv_default" assert provider["config"]["host"] == "${env.POSTGRES_HOST}" @@ -1162,12 +1170,12 @@ def test_enrich_vector_store_pgvector_no_kv_backend() -> None: assert provider["config"]["db"] == "${env.POSTGRES_DATABASE}" assert provider["config"]["user"] == "${env.POSTGRES_USER}" assert provider["config"]["password"] == "${env.POSTGRES_PASSWORD}" - assert "vsprov_nb-pg_storage" not in ls_config["storage"]["backends"] + assert "vsprov_nb-pg_storage" not in ogx_config["storage"]["backends"] def test_enrich_vector_store_multiple_entries() -> None: """Multi-entry list: both providers, faiss-only backend, default_provider winner.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": {}, "storage": {"backends": {}}, "registered_resources": {"models": [], "vector_stores": []}, @@ -1176,7 +1184,7 @@ def test_enrich_vector_store_multiple_entries() -> None: }, } enrich_vector_store( - ls_config, + ogx_config, { "default_provider": "notebooks", "providers": [ @@ -1200,27 +1208,27 @@ def test_enrich_vector_store_multiple_entries() -> None: ], }, ) - ids = {p["provider_id"] for p in ls_config["providers"]["vector_io"]} + ids = {p["provider_id"] for p in ogx_config["providers"]["vector_io"]} assert ids == {"notebooks", "nb-pg"} - assert "vsprov_notebooks_storage" in ls_config["storage"]["backends"] - assert "vsprov_nb-pg_storage" not in ls_config["storage"]["backends"] - assert ls_config["vector_stores"]["default_provider_id"] == "notebooks" - assert ls_config["vector_stores"]["default_embedding_model"]["model_id"] == ( + assert "vsprov_notebooks_storage" in ogx_config["storage"]["backends"] + assert "vsprov_nb-pg_storage" not in ogx_config["storage"]["backends"] + assert ogx_config["vector_stores"]["default_provider_id"] == "notebooks" + assert ogx_config["vector_stores"]["default_embedding_model"]["model_id"] == ( "vsprov_notebooks_embedding" ) assert ( - ls_config["vector_stores"]["annotation_prompt_params"]["enable_annotations"] + ogx_config["vector_stores"]["annotation_prompt_params"]["enable_annotations"] is False ) model_ids = { - m["provider_model_id"] for m in ls_config["registered_resources"]["models"] + m["provider_model_id"] for m in ogx_config["registered_resources"]["models"] } assert model_ids == {"/emb-faiss", "/emb-pg"} def test_enrich_vector_store_noop_without_entries() -> None: """Empty list leaves baseline vector_stores defaults unchanged.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": { "vector_io": [ {"provider_id": "faiss", "provider_type": "inline::faiss", "config": {}} @@ -1228,13 +1236,13 @@ def test_enrich_vector_store_noop_without_entries() -> None: }, "vector_stores": {"default_provider_id": "faiss"}, } - enrich_vector_store(ls_config, {"providers": []}) - assert ls_config["vector_stores"]["default_provider_id"] == "faiss" + enrich_vector_store(ogx_config, {"providers": []}) + assert ogx_config["vector_stores"]["default_provider_id"] == "faiss" def test_enrich_vector_store_registers_alias_when_load_path_shared_with_byok() -> None: """Shared provider_model_id with BYOK still registers vsprov_* for defaults.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": {}, "storage": {"backends": {}}, "registered_resources": { @@ -1252,7 +1260,7 @@ def test_enrich_vector_store_registers_alias_when_load_path_shared_with_byok() - "vector_stores": {}, } enrich_vector_store( - ls_config, + ogx_config, { "default_provider": "notebooks", "providers": [ @@ -1266,19 +1274,19 @@ def test_enrich_vector_store_registers_alias_when_load_path_shared_with_byok() - ], }, ) - model_ids = {m["model_id"] for m in ls_config["registered_resources"]["models"]} + model_ids = {m["model_id"] for m in ogx_config["registered_resources"]["models"]} assert model_ids == { "byok_rhdh-docs_embedding", "vsprov_notebooks_embedding", } - assert ls_config["vector_stores"]["default_embedding_model"]["model_id"] == ( + assert ogx_config["vector_stores"]["default_embedding_model"]["model_id"] == ( "vsprov_notebooks_embedding" ) def test_enrich_vector_store_dedupes_same_vsprov_model_id() -> None: """Re-enriching the same vector_store provider does not duplicate its model.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": {}, "storage": {"backends": {}}, "registered_resources": {"models": [], "vector_stores": []}, @@ -1296,25 +1304,25 @@ def test_enrich_vector_store_dedupes_same_vsprov_model_id() -> None: } ], } - enrich_vector_store(ls_config, vector_store) - enrich_vector_store(ls_config, vector_store) - assert len(ls_config["registered_resources"]["models"]) == 1 + enrich_vector_store(ogx_config, vector_store) + enrich_vector_store(ogx_config, vector_store) + assert len(ogx_config["registered_resources"]["models"]) == 1 assert ( - ls_config["registered_resources"]["models"][0]["model_id"] + ogx_config["registered_resources"]["models"][0]["model_id"] == "vsprov_notebooks_embedding" ) def test_enrich_vector_store_updates_vsprov_alias_on_path_change() -> None: """Re-enrichment with a new embedding path refreshes the vsprov_* model row.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": {}, "storage": {"backends": {}}, "registered_resources": {"models": [], "vector_stores": []}, "vector_stores": {}, } enrich_vector_store( - ls_config, + ogx_config, { "default_provider": "notebooks", "providers": [ @@ -1329,7 +1337,7 @@ def test_enrich_vector_store_updates_vsprov_alias_on_path_change() -> None: }, ) enrich_vector_store( - ls_config, + ogx_config, { "default_provider": "notebooks", "providers": [ @@ -1343,7 +1351,7 @@ def test_enrich_vector_store_updates_vsprov_alias_on_path_change() -> None: ], }, ) - models = ls_config["registered_resources"]["models"] + models = ogx_config["registered_resources"]["models"] assert len(models) == 1 assert models[0]["model_id"] == "vsprov_notebooks_embedding" assert models[0]["provider_model_id"] == "/new/embeddings_model" @@ -1352,14 +1360,14 @@ def test_enrich_vector_store_updates_vsprov_alias_on_path_change() -> None: def test_enrich_vector_store_skips_embedding_without_dimension() -> None: """embedding_model without embedding_dimension does not register a model.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": {}, "storage": {"backends": {}}, "registered_resources": {"models": []}, "vector_stores": {"default_provider_id": "faiss"}, } enrich_vector_store( - ls_config, + ogx_config, { "default_provider": "notebooks", "providers": [ @@ -1372,22 +1380,22 @@ def test_enrich_vector_store_skips_embedding_without_dimension() -> None: ], }, ) - assert ls_config["providers"]["vector_io"][0]["provider_id"] == "notebooks" - assert not ls_config["registered_resources"]["models"] - assert ls_config["vector_stores"]["default_provider_id"] == "notebooks" - assert "default_embedding_model" in ls_config["vector_stores"] + assert ogx_config["providers"]["vector_io"][0]["provider_id"] == "notebooks" + assert not ogx_config["registered_resources"]["models"] + assert ogx_config["vector_stores"]["default_provider_id"] == "notebooks" + assert "default_embedding_model" in ogx_config["vector_stores"] def test_enrich_vector_store_unmatched_default_provider_skips_defaults() -> None: """Unmatched default_provider still enriches providers but skips default_*.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": {}, "storage": {"backends": {}}, "registered_resources": {"models": []}, "vector_stores": {"default_provider_id": "faiss"}, } enrich_vector_store( - ls_config, + ogx_config, { "default_provider": "missing", "providers": [ @@ -1401,10 +1409,10 @@ def test_enrich_vector_store_unmatched_default_provider_skips_defaults() -> None ], }, ) - assert ls_config["providers"]["vector_io"][0]["provider_id"] == "notebooks" - assert ls_config["vector_stores"]["default_provider_id"] == "faiss" - assert "default_embedding_model" not in ls_config["vector_stores"] - assert len(ls_config["registered_resources"]["models"]) == 1 + assert ogx_config["providers"]["vector_io"][0]["provider_id"] == "notebooks" + assert ogx_config["vector_stores"]["default_provider_id"] == "faiss" + assert "default_embedding_model" not in ogx_config["vector_stores"] + assert len(ogx_config["registered_resources"]["models"]) == 1 # ============================================================================= diff --git a/tests/unit/test_ogx_synthesize.py b/tests/unit/test_ogx_synthesize.py index f58593b44..88d339465 100644 --- a/tests/unit/test_ogx_synthesize.py +++ b/tests/unit/test_ogx_synthesize.py @@ -42,9 +42,9 @@ # --------------------------------------------------------------------------- -def _tool_runtime_ids(ls_config: dict[str, Any]) -> list[Optional[str]]: +def _tool_runtime_ids(ogx_config: dict[str, Any]) -> list[Optional[str]]: """Return provider_id values from providers.tool_runtime.""" - providers = ls_config.get("providers") or {} + providers = ogx_config.get("providers") or {} return [ entry.get("provider_id") for entry in providers.get("tool_runtime") or [] @@ -52,19 +52,19 @@ def _tool_runtime_ids(ls_config: dict[str, Any]) -> list[Optional[str]]: ] -def _inference_entries(ls_config: dict[str, Any]) -> list[dict[str, Any]]: +def _inference_entries(ogx_config: dict[str, Any]) -> list[dict[str, Any]]: """Return inference provider dicts from a synthesized or baseline config.""" - providers = ls_config.get("providers") or {} + providers = ogx_config.get("providers") or {} return [ entry for entry in providers.get("inference") or [] if isinstance(entry, dict) ] -def _openai_inference_entries(ls_config: dict[str, Any]) -> list[dict[str, Any]]: +def _openai_inference_entries(ogx_config: dict[str, Any]) -> list[dict[str, Any]]: """Return remote::openai inference rows, including the conditional-id form.""" return [ entry - for entry in _inference_entries(ls_config) + for entry in _inference_entries(ogx_config) if entry.get("provider_type") == "remote::openai" or entry.get("provider_id") in ("openai", OPENAI_CONDITIONAL_PROVIDER_ID) ] @@ -72,7 +72,7 @@ def _openai_inference_entries(ls_config: dict[str, Any]) -> list[dict[str, Any]] def test_ensure_mcp_tool_runtime_appends_and_preserves_rag() -> None: """MCP is appended; existing rag-runtime is untouched.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "apis": ["tool_runtime"], "providers": { "tool_runtime": [ @@ -84,15 +84,15 @@ def test_ensure_mcp_tool_runtime_appends_and_preserves_rag() -> None: ] }, } - ensure_mcp_tool_runtime(ls_config) - assert _tool_runtime_ids(ls_config) == [ + ensure_mcp_tool_runtime(ogx_config) + assert _tool_runtime_ids(ogx_config) == [ "rag-runtime", "model-context-protocol", ] found = False found_entry = {} - for entry in ls_config["providers"]["tool_runtime"]: + for entry in ogx_config["providers"]["tool_runtime"]: if ( isinstance(entry, dict) and entry.get("provider_id") == "model-context-protocol" @@ -112,20 +112,20 @@ def test_ensure_mcp_tool_runtime_idempotent() -> None: "provider_type": "remote::model-context-protocol", "config": {"keep": True}, } - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "apis": ["tool_runtime"], "providers": {"tool_runtime": [existing]}, } - ensure_mcp_tool_runtime(ls_config) - assert ls_config["providers"]["tool_runtime"] == [existing] + ensure_mcp_tool_runtime(ogx_config) + assert ogx_config["providers"]["tool_runtime"] == [existing] def test_ensure_mcp_tool_runtime_adds_api_when_missing() -> None: """Thin baselines get tool_runtime in apis and the MCP provider.""" - ls_config: dict[str, Any] = {} - ensure_mcp_tool_runtime(ls_config) - assert "tool_runtime" in ls_config["apis"] - assert _tool_runtime_ids(ls_config) == ["model-context-protocol"] + ogx_config: dict[str, Any] = {} + ensure_mcp_tool_runtime(ogx_config) + assert "tool_runtime" in ogx_config["apis"] + assert _tool_runtime_ids(ogx_config) == ["model-context-protocol"] # --------------------------------------------------------------------------- @@ -267,7 +267,7 @@ def test_deep_merge_list_replace_does_not_mutate_inputs() -> None: def test_apply_high_level_inference_maps_type_and_emits_env_ref() -> None: """A remote provider maps to its provider_type with an ${env} api_key (R6).""" - ls_config: dict[str, Any] = {"providers": {"inference": []}} + ogx_config: dict[str, Any] = {"providers": {"inference": []}} inference = { "providers": [ { @@ -278,8 +278,8 @@ def test_apply_high_level_inference_maps_type_and_emits_env_ref() -> None: } ] } - apply_high_level_inference(ls_config, inference) - entry = ls_config["providers"]["inference"][0] + apply_high_level_inference(ogx_config, inference) + entry = ogx_config["providers"]["inference"][0] assert entry["provider_id"] == "openai" assert entry["provider_type"] == "remote::openai" assert entry["config"]["api_key"] == "${env.OPENAI_API_KEY}" @@ -288,10 +288,10 @@ def test_apply_high_level_inference_maps_type_and_emits_env_ref() -> None: def test_apply_high_level_inference_hyphenates_provider_id() -> None: """sentence_transformers emits the hyphenated id the ecosystem expects.""" - ls_config: dict[str, Any] = {"providers": {"inference": []}} + ogx_config: dict[str, Any] = {"providers": {"inference": []}} inference = {"providers": [{"type": "sentence_transformers"}]} - apply_high_level_inference(ls_config, inference) - entry = ls_config["providers"]["inference"][0] + apply_high_level_inference(ogx_config, inference) + entry = ogx_config["providers"]["inference"][0] assert entry["provider_id"] == "sentence-transformers" assert entry["provider_type"] == "inline::sentence-transformers" # no api_key / allowed_models -> no config block emitted @@ -302,7 +302,7 @@ def test_apply_high_level_inference_replaces_existing_provider_id( caplog: pytest.LogCaptureFixture, ) -> None: """A high-level provider replaces a baseline entry with the same id.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": { "inference": [ { @@ -316,17 +316,17 @@ def test_apply_high_level_inference_replaces_existing_provider_id( } inference = {"providers": [{"type": "openai", "api_key_env": "NEW_KEY"}]} with caplog.at_level("INFO", logger="lightspeed_stack.ogx_configuration"): - apply_high_level_inference(ls_config, inference) - ids = [p["provider_id"] for p in ls_config["providers"]["inference"]] + apply_high_level_inference(ogx_config, inference) + ids = [p["provider_id"] for p in ogx_config["providers"]["inference"]] assert ids == ["openai", "other"] # replaced in place, not duplicated - openai = ls_config["providers"]["inference"][0] + openai = ogx_config["providers"]["inference"][0] assert openai["config"]["api_key"] == "${env.NEW_KEY}" assert "provider_id='openai'" in caplog.text def test_apply_high_level_inference_replaces_conditional_provider_id() -> None: """The baseline ${env.OPENAI_API_KEY:+openai} row matches id openai.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": { "inference": [ { @@ -342,18 +342,18 @@ def test_apply_high_level_inference_replaces_conditional_provider_id() -> None: } } inference = {"providers": [{"type": "openai", "api_key_env": "OPENAI_API_KEY"}]} - apply_high_level_inference(ls_config, inference) - openai_entries = _openai_inference_entries(ls_config) + apply_high_level_inference(ogx_config, inference) + openai_entries = _openai_inference_entries(ogx_config) assert len(openai_entries) == 1 assert openai_entries[0]["provider_id"] == "openai" assert openai_entries[0]["config"]["api_key"] == "${env.OPENAI_API_KEY}" - ids = [entry["provider_id"] for entry in _inference_entries(ls_config)] + ids = [entry["provider_id"] for entry in _inference_entries(ogx_config)] assert ids == ["openai", "sentence-transformers"] def test_apply_high_level_inference_uses_explicit_id() -> None: """An explicit id is emitted as provider_id instead of the type-derived id.""" - ls_config: dict[str, Any] = {"providers": {"inference": []}} + ogx_config: dict[str, Any] = {"providers": {"inference": []}} inference = { "providers": [ { @@ -363,15 +363,15 @@ def test_apply_high_level_inference_uses_explicit_id() -> None: } ] } - apply_high_level_inference(ls_config, inference) - entry = ls_config["providers"]["inference"][0] + apply_high_level_inference(ogx_config, inference) + entry = ogx_config["providers"]["inference"][0] assert entry["provider_id"] == "vllm-prod" assert entry["provider_type"] == "remote::vllm" def test_apply_high_level_inference_same_type_distinct_ids() -> None: """Two providers of the same type with distinct ids both appear.""" - ls_config: dict[str, Any] = {"providers": {"inference": []}} + ogx_config: dict[str, Any] = {"providers": {"inference": []}} inference = { "providers": [ { @@ -388,8 +388,8 @@ def test_apply_high_level_inference_same_type_distinct_ids() -> None: }, ] } - apply_high_level_inference(ls_config, inference) - by_id = {e["provider_id"]: e for e in ls_config["providers"]["inference"]} + apply_high_level_inference(ogx_config, inference) + by_id = {e["provider_id"]: e for e in ogx_config["providers"]["inference"]} assert set(by_id) == {"vllm-prod", "vllm-staging"} assert all(e["provider_type"] == "remote::vllm" for e in by_id.values()) assert by_id["vllm-prod"]["config"]["url"] == "http://prod:8000" @@ -400,7 +400,7 @@ def test_apply_high_level_inference_duplicate_id_last_wins( caplog: pytest.LogCaptureFixture, ) -> None: """Duplicate id keeps the last entry and logs an info message.""" - ls_config: dict[str, Any] = {"providers": {"inference": []}} + ogx_config: dict[str, Any] = {"providers": {"inference": []}} inference = { "providers": [ { @@ -416,8 +416,8 @@ def test_apply_high_level_inference_duplicate_id_last_wins( ] } with caplog.at_level("INFO", logger="lightspeed_stack.ogx_configuration"): - apply_high_level_inference(ls_config, inference) - entries = ls_config["providers"]["inference"] + apply_high_level_inference(ogx_config, inference) + entries = ogx_config["providers"]["inference"] assert len(entries) == 1 assert entries[0]["provider_id"] == "vllm-shared" assert entries[0]["config"]["api_token"] == "${env.SECOND_KEY}" @@ -426,14 +426,14 @@ def test_apply_high_level_inference_duplicate_id_last_wins( def test_apply_high_level_inference_merges_extra() -> None: """The extra mapping is merged verbatim into the provider config block.""" - ls_config: dict[str, Any] = {"providers": {"inference": []}} + ogx_config: dict[str, Any] = {"providers": {"inference": []}} inference = { "providers": [ {"type": "vllm_rhaiis", "extra": {"url": "http://x", "tls_verify": False}} ] } - apply_high_level_inference(ls_config, inference) - entry = ls_config["providers"]["inference"][0] + apply_high_level_inference(ogx_config, inference) + entry = ogx_config["providers"]["inference"][0] assert entry["provider_id"] == "vllm-rhaiis" assert entry["provider_type"] == "remote::vllm" assert entry["config"] == {"url": "http://x", "tls_verify": False} @@ -441,16 +441,16 @@ def test_apply_high_level_inference_merges_extra() -> None: def test_apply_high_level_inference_emits_api_token_for_vllm() -> None: """vLLM providers emit api_token from api_key_env, not api_key.""" - ls_config: dict[str, Any] = {"providers": {"inference": []}} + ogx_config: dict[str, Any] = {"providers": {"inference": []}} inference = { "providers": [ {"type": "vllm", "api_key_env": "VLLM_API_KEY"}, {"type": "vllm_rhaiis", "api_key_env": "VLLM_API_KEY"}, ] } - apply_high_level_inference(ls_config, inference) - vllm = ls_config["providers"]["inference"][0] - vllm_rhaiis = ls_config["providers"]["inference"][1] + apply_high_level_inference(ogx_config, inference) + vllm = ogx_config["providers"]["inference"][0] + vllm_rhaiis = ogx_config["providers"]["inference"][1] assert vllm["provider_id"] == "vllm" assert vllm["provider_type"] == "remote::vllm" assert vllm["config"]["api_token"] == "${env.VLLM_API_KEY}" @@ -462,14 +462,14 @@ def test_apply_high_level_inference_emits_api_token_for_vllm() -> None: def test_apply_high_level_inference_maps_ollama() -> None: """ollama maps to remote::ollama with extra config merged.""" - ls_config: dict[str, Any] = {"providers": {"inference": []}} + ogx_config: dict[str, Any] = {"providers": {"inference": []}} inference = { "providers": [ {"type": "ollama", "extra": {"base_url": "http://localhost:11434"}} ] } - apply_high_level_inference(ls_config, inference) - entry = ls_config["providers"]["inference"][0] + apply_high_level_inference(ogx_config, inference) + entry = ogx_config["providers"]["inference"][0] assert entry["provider_id"] == "ollama" assert entry["provider_type"] == "remote::ollama" assert entry["config"]["base_url"] == "http://localhost:11434" @@ -477,7 +477,7 @@ def test_apply_high_level_inference_maps_ollama() -> None: def test_apply_high_level_inference_maps_vllm() -> None: """vllm maps to remote::vllm with extra config merged.""" - ls_config: dict[str, Any] = {"providers": {"inference": []}} + ogx_config: dict[str, Any] = {"providers": {"inference": []}} inference = { "providers": [ { @@ -487,8 +487,8 @@ def test_apply_high_level_inference_maps_vllm() -> None: } ] } - apply_high_level_inference(ls_config, inference) - entry = ls_config["providers"]["inference"][0] + apply_high_level_inference(ogx_config, inference) + entry = ogx_config["providers"]["inference"][0] assert entry["provider_id"] == "vllm" assert entry["provider_type"] == "remote::vllm" assert entry["config"]["api_token"] == "${env.VLLM_API_KEY}" @@ -497,7 +497,7 @@ def test_apply_high_level_inference_maps_vllm() -> None: def test_apply_high_level_inference_extra_cannot_override_api_key_env() -> None: """api_key_env always wins over a conflicting key in extra.""" - ls_config: dict[str, Any] = {"providers": {"inference": []}} + ogx_config: dict[str, Any] = {"providers": {"inference": []}} inference = { "providers": [ { @@ -507,8 +507,8 @@ def test_apply_high_level_inference_extra_cannot_override_api_key_env() -> None: } ] } - apply_high_level_inference(ls_config, inference) - entry = ls_config["providers"]["inference"][0] + apply_high_level_inference(ogx_config, inference) + entry = ogx_config["providers"]["inference"][0] assert entry["config"]["api_token"] == "${env.VLLM_API_KEY}" @@ -522,9 +522,9 @@ def test_unified_inference_provider_accepts_ollama_and_vllm() -> None: def test_apply_high_level_inference_empty_is_noop() -> None: """No providers -> the inference list is left as-is.""" - ls_config: dict[str, Any] = {"providers": {"inference": [{"provider_id": "x"}]}} - apply_high_level_inference(ls_config, {"providers": []}) - assert ls_config["providers"]["inference"] == [{"provider_id": "x"}] + ogx_config: dict[str, Any] = {"providers": {"inference": [{"provider_id": "x"}]}} + apply_high_level_inference(ogx_config, {"providers": []}) + assert ogx_config["providers"]["inference"] == [{"provider_id": "x"}] def test_provider_type_map_covers_every_literal_value() -> None: diff --git a/tests/unit/utils/dumpers/test_models_dumper.py b/tests/unit/utils/dumpers/test_models_dumper.py index 684a61c89..2dce7605a 100644 --- a/tests/unit/utils/dumpers/test_models_dumper.py +++ b/tests/unit/utils/dumpers/test_models_dumper.py @@ -6756,7 +6756,7 @@ def test_dump_models(tmpdir: Path) -> None: "type": "string" }, "conversation": { - "description": "The conversation ID in llama-stack format", + "description": "The conversation ID in OGX format", "title": "Conversation", "type": "string" }, @@ -9035,7 +9035,7 @@ def test_dump_models(tmpdir: Path) -> None: "type": "object" }, "TurnSummary": { - "description": "Summary of a turn in llama stack.", + "description": "Summary of a turn in OGX.", "properties": { "id": { "default": "", diff --git a/tests/unit/utils/test_compaction.py b/tests/unit/utils/test_compaction.py index ab146b4e4..0eda1dcce 100644 --- a/tests/unit/utils/test_compaction.py +++ b/tests/unit/utils/test_compaction.py @@ -74,11 +74,11 @@ def _make_history(num_pairs: int, words_per_message: int = 1) -> list[Any]: class TestIsMessageItem: """Tests for is_message_item.""" - def test_llama_stack_message(self) -> None: + def test_ogx_message(self) -> None: """OGX message item is recognised.""" assert is_message_item(_MessageItem("user", "hi")) is True - def test_llama_stack_tool_call(self) -> None: + def test_ogx_tool_call(self) -> None: """Tool-call item is not a message.""" assert is_message_item(_ToolCallItem()) is False diff --git a/tests/unit/utils/test_token_estimator.py b/tests/unit/utils/test_token_estimator.py index a068c6b7d..fe62157bf 100644 --- a/tests/unit/utils/test_token_estimator.py +++ b/tests/unit/utils/test_token_estimator.py @@ -115,11 +115,11 @@ def test_within_5pct_of_explicit_tiktoken_call(self) -> None: class TestIsMessage: """Tests for the is_message_item duck-type check.""" - def test_llama_stack_message_item(self) -> None: + def test_ogx_message_item(self) -> None: """An OGX-shaped object with type == 'message' is a message.""" assert is_message_item(_MessageItem("user", "hi")) is True - def test_llama_stack_tool_call_item(self) -> None: + def test_ogx_tool_call_item(self) -> None: """A tool-call-shaped object is not a message.""" assert is_message_item(_ToolCallItem()) is False @@ -137,7 +137,7 @@ def test_dict_is_not_a_message(self) -> None: class TestExtractMessageText: """Tests for the extract_message_text duck-type extractor.""" - def test_llama_stack_string_content(self) -> None: + def test_ogx_string_content(self) -> None: """Plain string content is returned as-is.""" assert extract_message_text(_MessageItem("user", "hello")) == "hello" From a2aae40722143a7800f56a86adafb2f4f9f5428a Mon Sep 17 00:00:00 2001 From: Andrej Simurka Date: Fri, 11 Sep 2026 12:04:22 +0200 Subject: [PATCH 052/120] update user and developer docs to OGX naming --- README.md | 44 +++---- docs/basic_info/getting_started.md | 4 +- docs/basic_info/overview.svg | 6 +- docs/demos/lcore/LnL_2026.md | 8 +- .../lcore/images/llama_stack_providers.svg | 4 - .../{llama_stack_arch.png => ogx_arch.png} | Bin .../{llama_stack_arch.svg => ogx_arch.svg} | 8 +- ...tack_as_library.svg => ogx_as_library.svg} | 4 +- ...tack_as_service.svg => ogx_as_service.svg} | 4 +- ..._in_container.svg => ogx_in_container.svg} | 4 +- docs/demos/lcore/images/ogx_providers.svg | 4 + docs/demos/lcore/lcore.md | 30 ++--- .../byok-confluence-import.md | 2 +- docs/design/byok-pdf/byok-pdf.md | 2 +- .../conversation-compaction.md | 2 +- .../human-in-the-loop-spike.md | 6 +- .../human-in-the-loop/human-in-the-loop.md | 8 +- .../sequence_diagram.puml | 4 +- .../sequence_diagram.svg | 25 ++-- .../ogx-config-merge-spike.md | 117 +++++++++--------- .../ogx-config-merge/ogx-config-merge.md | 101 +++++++-------- .../prompt-guardrails/poc-results/README.md | 2 +- .../poc-results/lcs-poc-config.yaml | 2 +- .../prompt-guardrails-spike.md | 10 +- .../prompt-guardrails/prompt-guardrails.md | 6 +- docs/devel_doc/ARCHITECTURE.md | 2 +- docs/devel_doc/architecture.svg | 10 +- docs/devel_doc/container_orchestration.md | 32 ++--- docs/devel_doc/conversation_history.svg | 10 +- docs/devel_doc/conversations_api.md | 11 +- ...k_interface.png => core2ogx_interface.png} | Bin docs/devel_doc/openapi.md | 2 +- docs/devel_doc/persistent_storage.svg | 18 +-- docs/devel_doc/providers.md | 18 +-- docs/devel_doc/query_endpoint.svg | 8 +- docs/devel_doc/streaming_query_endpoint.svg | 10 +- docs/user_doc/a2a_protocol.md | 2 +- docs/user_doc/both_services_in_container.svg | 4 +- docs/user_doc/byok_guide.md | 4 +- docs/user_doc/config.html | 2 +- docs/user_doc/config.json | 2 +- docs/user_doc/config.md | 2 +- docs/user_doc/config.puml | 12 +- docs/user_doc/config.svg | 40 +++--- docs/user_doc/deployment_guide.md | 71 ++++++----- docs/user_doc/lcs_in_container.svg | 4 +- ...tack_as_library.svg => ogx_as_library.svg} | 4 +- ...tack_as_service.svg => ogx_as_service.svg} | 4 +- docs/user_doc/opentelemetry.md | 2 +- docs/user_doc/rag_guide.md | 10 +- docs/user_doc/splunk.md | 2 +- ...ightspeed-stack-azure-entraid-service.yaml | 2 +- examples/profiles/inline-faiss.yaml | 2 +- examples/profiles/openai-remote.yaml | 2 +- ...ect.llamastack.toml => pyproject.ogx.toml} | 6 +- examples/quota-limiter-configuration-pg.yaml | 4 +- .../quota-limiter-configuration-sqlite.yaml | 4 +- scripts/ogx_tutorial.sh | 68 +++++----- .../llamastack/README.md | 2 - .../llamastack/README.md | 2 - 60 files changed, 400 insertions(+), 385 deletions(-) delete mode 100644 docs/demos/lcore/images/llama_stack_providers.svg rename docs/demos/lcore/images/{llama_stack_arch.png => ogx_arch.png} (100%) rename docs/demos/lcore/images/{llama_stack_arch.svg => ogx_arch.svg} (95%) rename docs/demos/lcore/images/{llama_stack_as_library.svg => ogx_as_library.svg} (99%) rename docs/demos/lcore/images/{llama_stack_as_service.svg => ogx_as_service.svg} (99%) rename docs/demos/lcore/images/{llama_stack_in_container.svg => ogx_in_container.svg} (96%) create mode 100644 docs/demos/lcore/images/ogx_providers.svg rename docs/devel_doc/{core2llama-stack_interface.png => core2ogx_interface.png} (100%) rename docs/user_doc/{llama_stack_as_library.svg => ogx_as_library.svg} (99%) rename docs/user_doc/{llama_stack_as_service.svg => ogx_as_service.svg} (99%) rename examples/{pyproject.llamastack.toml => pyproject.ogx.toml} (89%) delete mode 100644 src/pydantic_ai_lightspeed/llamastack/README.md delete mode 100644 tests/unit/pydantic_ai_lightspeed/llamastack/README.md diff --git a/README.md b/README.md index 402995ada..c7ef35754 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,7 @@ To quickly get hands on LCS, we can run it using the default configurations prov ``` 4. access LCS web UI at [http://localhost:8080/](http://localhost:8080/) -**Note**: `make run` uses containerized OGX (service mode). For details on container lifecycle management, customization, and troubleshooting, see the [Container Orchestration Guide](docs/devel_doc/container_orchestration.md). To run llama-stack manually instead, see the [OGX as separate server](#ogx-as-separate-server) section below. +**Note**: `make run` uses containerized OGX (service mode). For details on container lifecycle management, customization, and troubleshooting, see the [Container Orchestration Guide](docs/devel_doc/container_orchestration.md). To run OGX manually instead, see the [OGX as separate server](#ogx-as-separate-server) section below. ## Container Runtime Requirements @@ -337,7 +337,7 @@ Kotlin, which "wraps" the REST API stack in a suitable way, which is easier for many applications. -![Integration with OGX](docs/core2llama-stack_interface.png) +![Integration with OGX](docs/devel_doc/core2ogx_interface.png) @@ -354,7 +354,7 @@ service: workers: 1 color_log: true access_log: true -llama_stack: +ogx: use_as_library_client: false url: http://localhost:8321 user_data_collection: @@ -366,7 +366,7 @@ user_data_collection: ### Degraded mode -Lightspeed core is able to continue operating in a _degraded but safe_ mode if the LLS service is not started or fails to start. When degraded, the `/health` endpoint report the LLS status and any relevant impacts so operators and automation can detect and respond. +Lightspeed core is able to continue operating in a _degraded but safe_ mode if the OGX service is not started or fails to start. When degraded, the `/health` endpoint report the OGX status and any relevant impacts so operators and automation can detect and respond. Degraded mode need to be enabled in `lightspeed-stack.yaml` configuration file: @@ -620,12 +620,12 @@ To run OGX in separate process, you need to have all dependencies installed. The ```toml [project] -name = "llama-stack-runner" +name = "ogx-runner" version = "0.1.0" description = "OGX runner" authors = [] dependencies = [ - "llama-stack==0.2.22", + "ogx==1.2.5", "fastapi>=0.115.12", "opentelemetry-sdk>=1.34.0", "opentelemetry-exporter-otlp>=1.34.0", @@ -659,7 +659,7 @@ To run OGX perform these two commands: ``` export OPENAI_API_KEY="sk-{YOUR-KEY}" -uv run llama stack run run.yaml +uv run ogx stack run run.yaml ``` ### Check connection to OGX @@ -683,14 +683,14 @@ service: workers: 1 color_log: true access_log: true -llama_stack: +ogx: use_as_library_client: true # Unified mode (recommended): LCORE synthesizes the OGX run.yaml. # Point profile at a run.yaml-shaped file you author, or omit the config # block and drive everything from the top-level inference.providers # section over the built-in default baseline. config: - profile: + profile: user_data_collection: feedback_enabled: true feedback_storage: "/tmp/data/feedback" @@ -706,7 +706,7 @@ user_data_collection: ## OGX version check -During Lightspeed Core Stack service startup, the OGX version is retrieved. The version is tested against two constants `MINIMAL_SUPPORTED_LLAMA_STACK_VERSION` and `MAXIMAL_SUPPORTED_LLAMA_STACK_VERSION` which are defined in `src/constants.py`. If the actual OGX version is outside the range defined by these two constants, the service won't start and administrator will be informed about this problem. +During Lightspeed Core Stack service startup, the OGX version is retrieved. The version is tested against two constants `MINIMAL_SUPPORTED_OGX_VERSION` and `MAXIMAL_SUPPORTED_OGX_VERSION` which are defined in `src/constants.py`. If the actual OGX version is outside the range defined by these two constants, the service won't start and administrator will be informed about this problem. @@ -888,7 +888,7 @@ options: path where the synthesized OGX run.yaml is written in unified library mode (overwritten each boot, mode 0600; default: ./.generated/run.yaml) --migrate-config migrate a legacy two-file config to a unified single file and exit. Lifts the run.yaml given by --run-yaml - into the llama_stack.config.native_override of the -c lightspeed-stack.yaml and writes the result to + into the ogx.config.native_override of the -c lightspeed-stack.yaml and writes the result to --migrate-output. Replace literal secrets with ${env.VAR} references before or after migrating. --run-yaml RUN_YAML path to the legacy OGX run.yaml to migrate (used with --migrate-config) --migrate-output MIGRATE_OUTPUT @@ -918,15 +918,15 @@ Usage: make ... Available targets are: -run-stack Run lightspeed-stack directly, without building dependent service/s +run-ogx Run lightspeed-stack directly, without building dependent service/s run Run the service locally with dependent services -build-llama-stack-image Build OGX container image -stop-llama-stack-container Gracefully stop OGX container -remove-llama-stack-container Remove OGX container (saves logs first) -start-llama-stack-container Start OGX container -wait-for-llama-stack-health Wait for OGX container to be healthy -clean-llama-stack Remove container and image -run-llama-stack Start OGX with enriched config (for local service mode) +build-ogx-image Build OGX container image +stop-ogx-container Gracefully stop OGX container +remove-ogx-container Remove OGX container (saves logs first) +start-ogx-container Start OGX container +wait-for-ogx-health Wait for OGX container to be healthy +clean-ogx Remove container and image +run-ogx-local Start OGX with enriched config (for local service mode) test-unit Run the unit tests test-integration Run integration tests tests test-e2e Run end to end tests for the service @@ -1018,9 +1018,9 @@ When using OGX as a separate service, the existing `docker-compose.yaml` provide **Configuration** (`lightspeed-stack.yaml`): ```yaml -llama_stack: +ogx: use_as_library_client: false - url: http://llama-stack:8321 # container name from docker-compose.yaml + url: http://ogx:8321 # service name from docker-compose.yaml api_key: xyzzy ``` @@ -1057,7 +1057,7 @@ When embedding OGX directly in the container, use the existing `deploy/lightspee **Configuration** (`lightspeed-stack.yaml`): ```yaml -llama_stack: +ogx: use_as_library_client: true # Unified mode: the mounted run.yaml is the synthesis profile. (The # legacy library_client_config_path equivalent is deprecated, removed diff --git a/docs/basic_info/getting_started.md b/docs/basic_info/getting_started.md index 0b17e6e7f..4377501c7 100644 --- a/docs/basic_info/getting_started.md +++ b/docs/basic_info/getting_started.md @@ -24,7 +24,9 @@ It is possible to run Lightspeed Core Stack service with OGX "embedded" as a Pyt 1. Add and install all required dependencies ```bash uv add \ - "llama-stack==0.2.22" \ + "ogx==1.2.5" \ + "ogx-api==1.2.5" \ + "ogx-client==1.2.5" \ "fastapi>=0.115.12" \ "opentelemetry-sdk>=1.34.0" \ "opentelemetry-exporter-otlp>=1.34.0" \ diff --git a/docs/basic_info/overview.svg b/docs/basic_info/overview.svg index 02dd918d4..3f32b9bc5 100644 --- a/docs/basic_info/overview.svg +++ b/docs/basic_info/overview.svg @@ -1,7 +1,7 @@ - + @@ -168,7 +168,7 @@

-
Llama Stack
+
OGX
(library or
service mode)
@@ -453,7 +453,7 @@
-
Llama Stack
Configuration
+
OGX
Configuration
diff --git a/docs/demos/lcore/LnL_2026.md b/docs/demos/lcore/LnL_2026.md index fa707457b..f8155df2b 100644 --- a/docs/demos/lcore/LnL_2026.md +++ b/docs/demos/lcore/LnL_2026.md @@ -34,25 +34,25 @@ --- -![Architecture](images/llama_stack_arch.png) +![Architecture](images/ogx_arch.png) --- ### OGX as a library -![LS1](images/llama_stack_as_library.svg) +![LS1](images/ogx_as_library.svg) --- ### OGX as a service -![LS2](images/llama_stack_as_service.svg) +![LS2](images/ogx_as_service.svg) --- ### Running inside container -![LS3](images/llama_stack_in_container.svg) +![LS3](images/ogx_in_container.svg) --- diff --git a/docs/demos/lcore/images/llama_stack_providers.svg b/docs/demos/lcore/images/llama_stack_providers.svg deleted file mode 100644 index d05050df2..000000000 --- a/docs/demos/lcore/images/llama_stack_providers.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - -
Persistent storage
Service that calls
Llama Stack
llama-stack
Agents
RAG
Conversation cache
OKP
Question validators
Answer redactors
Summarizers
MCP servers
Llama Stack
Configuration






PostgreSQL






SQLite
Persistent storage






PostgreSQL






SQLite
Persistent storage






PostgreSQL






SQLite
diff --git a/docs/demos/lcore/images/llama_stack_arch.png b/docs/demos/lcore/images/ogx_arch.png similarity index 100% rename from docs/demos/lcore/images/llama_stack_arch.png rename to docs/demos/lcore/images/ogx_arch.png diff --git a/docs/demos/lcore/images/llama_stack_arch.svg b/docs/demos/lcore/images/ogx_arch.svg similarity index 95% rename from docs/demos/lcore/images/llama_stack_arch.svg rename to docs/demos/lcore/images/ogx_arch.svg index a70042645..fda50ce3b 100644 --- a/docs/demos/lcore/images/llama_stack_arch.svg +++ b/docs/demos/lcore/images/ogx_arch.svg @@ -1,7 +1,7 @@ - + @@ -285,7 +285,7 @@
-
Llama Stack
+
OGX
(library or
service mode)
@@ -677,7 +677,7 @@
-
Llama Stack
Configuration
+
OGX
Configuration
@@ -1319,7 +1319,7 @@
-
Llama Stack
+
OGX
configuration
generator
diff --git a/docs/demos/lcore/images/llama_stack_as_library.svg b/docs/demos/lcore/images/ogx_as_library.svg similarity index 99% rename from docs/demos/lcore/images/llama_stack_as_library.svg rename to docs/demos/lcore/images/ogx_as_library.svg index bc2efe117..67f7a9d16 100644 --- a/docs/demos/lcore/images/llama_stack_as_library.svg +++ b/docs/demos/lcore/images/ogx_as_library.svg @@ -102,12 +102,12 @@
-
Llama Stack as a library (Python package)
+
OGX as a library (Python package)
- Llama Stack library + OGX library diff --git a/docs/demos/lcore/images/llama_stack_as_service.svg b/docs/demos/lcore/images/ogx_as_service.svg similarity index 99% rename from docs/demos/lcore/images/llama_stack_as_service.svg rename to docs/demos/lcore/images/ogx_as_service.svg index 1a53f2b11..2bb303837 100644 --- a/docs/demos/lcore/images/llama_stack_as_service.svg +++ b/docs/demos/lcore/images/ogx_as_service.svg @@ -126,12 +126,12 @@
-
Llama Stack service
+
OGX service
- Llama Stack service + OGX service diff --git a/docs/demos/lcore/images/llama_stack_in_container.svg b/docs/demos/lcore/images/ogx_in_container.svg similarity index 96% rename from docs/demos/lcore/images/llama_stack_in_container.svg rename to docs/demos/lcore/images/ogx_in_container.svg index a17f6aa76..c26ab020d 100644 --- a/docs/demos/lcore/images/llama_stack_in_container.svg +++ b/docs/demos/lcore/images/ogx_in_container.svg @@ -1,7 +1,7 @@ - + @@ -150,7 +150,7 @@
-
Llama Stack server
+
OGX server
diff --git a/docs/demos/lcore/images/ogx_providers.svg b/docs/demos/lcore/images/ogx_providers.svg new file mode 100644 index 000000000..0c7038ae2 --- /dev/null +++ b/docs/demos/lcore/images/ogx_providers.svg @@ -0,0 +1,4 @@ + + + +
Persistent storage
Service that calls
OGX
ogx
Agents
RAG
Conversation cache
OKP
Question validators
Answer redactors
Summarizers
MCP servers
OGX
Configuration






PostgreSQL






SQLite
Persistent storage






PostgreSQL






SQLite
Persistent storage






PostgreSQL






SQLite
diff --git a/docs/demos/lcore/lcore.md b/docs/demos/lcore/lcore.md index 9b7395278..fd4886487 100644 --- a/docs/demos/lcore/lcore.md +++ b/docs/demos/lcore/lcore.md @@ -22,7 +22,7 @@ ptisnovs@redhat.com ## OGX -![LCORE](images/llama_stack_logo.png) +![OGX](images/ogx_arch.png) --- @@ -38,7 +38,7 @@ ptisnovs@redhat.com --- -![LS1](images/llama_stack.png) +![OGX architecture](images/ogx_arch.svg) --- @@ -135,10 +135,6 @@ ptisnovs@redhat.com --- -![LS-providers](images/llama_stack_providers.svg) - ---- - ### Communication with OGX * CLI @@ -161,19 +157,19 @@ ptisnovs@redhat.com ### OGX as a library -![LS1](images/llama_stack_as_library.svg) +![LS1](images/ogx_as_library.svg) --- ### OGX as a service -![LS1](images/llama_stack_as_service.svg) +![LS1](images/ogx_as_service.svg) --- ### Run inside container -![LS1](images/llama_stack_in_container.svg) +![LS1](images/ogx_in_container.svg) --- @@ -185,7 +181,7 @@ Python ecosystem ``` pdm init -pdm add llama-stack fastapi opentelemetry-sdk \ +pdm add ogx ogx-api ogx-client fastapi opentelemetry-sdk \ opentelemetry-exporter-otlp opentelemetry-instrumentation \ aiosqlite litellm uvicorn blobfile ``` @@ -196,13 +192,13 @@ aiosqlite litellm uvicorn blobfile ```toml [project] -name = "llama-stack-demo" +name = "ogx-demo" version = "0.1.0" description = "Default template for PDM package" authors = [] dependencies = [ - "llama-stack==0.2.20", - "llama-stack-client==0.2.20", + "ogx==1.2.5", + "ogx-client==1.2.5", "opentelemetry-sdk>=1.34.0", "opentelemetry-exporter-otlp>=1.34.0", "opentelemetry-instrumentation>=0.55b0", @@ -224,7 +220,7 @@ distribution = false ### Starting OGX ```bash -uv run llama stack run +uv run ogx stack run ``` --- @@ -232,7 +228,7 @@ uv run llama stack run ### List of OGX API ```bash -uv run llama stack list-apis +uv run ogx stack list-apis ``` --- @@ -290,7 +286,7 @@ uv run llama stack list-apis ### List of providers ```bash -uv run llama stack list-providers +uv run ogx stack list-providers ``` --- @@ -343,7 +339,7 @@ uv run llama stack list-providers --- -![LS1](images/llama_stack_arch.svg) +![LS1](images/ogx_arch.svg) --- diff --git a/docs/design/byok-confluence-import/byok-confluence-import.md b/docs/design/byok-confluence-import/byok-confluence-import.md index 6277490d6..60a6686f7 100644 --- a/docs/design/byok-confluence-import/byok-confluence-import.md +++ b/docs/design/byok-confluence-import/byok-confluence-import.md @@ -44,7 +44,7 @@ unchanged pages). - **R4:** Every chunk in the built store carries the source page's title and canonical Confluence URL, so answers can cite the page. - **R5:** A single command performs fetch → build → artifact - (`llamastack-faiss` file by default; optional OCI image via the existing + (`llamastack-faiss` rag-content artifact name; optional OCI image via the existing `--output-image`). - **R6:** Incremental refresh: a re-run against unchanged spaces performs no page-body fetches and no re-embeddings; changed pages are re-imported; diff --git a/docs/design/byok-pdf/byok-pdf.md b/docs/design/byok-pdf/byok-pdf.md index 1a82db734..b2ad11ee6 100644 --- a/docs/design/byok-pdf/byok-pdf.md +++ b/docs/design/byok-pdf/byok-pdf.md @@ -114,7 +114,7 @@ These are baked into `PDFReader.__init__`. **No CLI flags expose them in v1.** I PDFs go through the same chunking path as HTML and Markdown. Two predicates in `document_processor.py` need to learn `"pdf"`: - The `Settings.node_parser = MarkdownNodeParser()` branch in `_BaseDB.__init__`. -- The same branch in `_LlamaStackDB.__init__` (currently a separate copy of the predicate). +- The same branch in `_LlamaStackDB.__init__` (rag-content class name; currently a separate copy of the predicate). To prevent these two tuples from drifting, extract a single module-level constant and use it from both call sites: diff --git a/docs/design/conversation-compaction/conversation-compaction.md b/docs/design/conversation-compaction/conversation-compaction.md index 6cdb9991d..455d9acb0 100644 --- a/docs/design/conversation-compaction/conversation-compaction.md +++ b/docs/design/conversation-compaction/conversation-compaction.md @@ -352,7 +352,7 @@ Use the same pattern as `conversations_v1.py:240-246`: ``` python items_response = await client.conversations.items.list( - conversation_id=llama_stack_conv_id, + conversation_id=ogx_conv_id, after=None, include=None, limit=None, diff --git a/docs/design/human-in-the-loop/human-in-the-loop-spike.md b/docs/design/human-in-the-loop/human-in-the-loop-spike.md index 7195b430e..cb1b901d5 100644 --- a/docs/design/human-in-the-loop/human-in-the-loop-spike.md +++ b/docs/design/human-in-the-loop/human-in-the-loop-spike.md @@ -325,8 +325,8 @@ Reference existing docs in docs/ for style. No PoC was built for this spike. The core mechanisms are already validated: -1. **OGX approval types exist**: `MCPApprovalRequest` and - `MCPApprovalResponse` are defined in `llama_stack_api.openai_responses` +1. **OGX approval types exist**: `OpenAIResponseMCPApprovalRequest` and + `OpenAIResponseMCPApprovalResponse` are defined in `ogx_api.openai_responses` 2. **LCS already parses approval events**: `build_tool_call_summary()` in [responses.py:1067-1094](../../../src/utils/responses.py#L1067-L1094) handles both `mcp_approval_request` and `mcp_approval_response` types @@ -369,7 +369,7 @@ async def get_mcp_tools(...) -> list[InputToolMCP]: ### OGX Support -From `llama_stack_api.openai_responses`: +From `ogx_api.openai_responses`: ```python class ApprovalFilter(BaseModel): diff --git a/docs/design/human-in-the-loop/human-in-the-loop.md b/docs/design/human-in-the-loop/human-in-the-loop.md index dfb884f78..a513b683b 100644 --- a/docs/design/human-in-the-loop/human-in-the-loop.md +++ b/docs/design/human-in-the-loop/human-in-the-loop.md @@ -7,7 +7,7 @@ | **Authors** | Lightspeed Core Team | | **Feature** | [LCORE-268](https://redhat.atlassian.net/browse/LCORE-268) | | **Spike** | [LCORE-1589](https://redhat.atlassian.net/browse/LCORE-1589) | -| **Links** | [MCP Spec](https://modelcontextprotocol.io), [OGX](https://github.com/meta-llama/llama-stack) | +| **Links** | [MCP Spec](https://modelcontextprotocol.io), [OGX](https://github.com/ogx-ai/ogx) | ## What @@ -527,10 +527,10 @@ async def get_mcp_tools(...) -> list[InputToolMCP]: require_approval = mcp_server.require_approval if isinstance(require_approval, ApprovalFilter): # Convert to OGX's ApprovalFilter format - require_approval = LlamaStackApprovalFilter( + require_approval = ApprovalFilter( always=require_approval.always or None, never=require_approval.never or None, - ) + ) # ogx_api.openai_responses.ApprovalFilter tools.append( InputToolMCP( @@ -596,7 +596,7 @@ Example config files go in `examples/`. ## Appendix A: OGX Types Reference -From `llama_stack_api.openai_responses`: +From `ogx_api.openai_responses`: ```python class ApprovalFilter(BaseModel): diff --git a/docs/design/low-overhead-deployment-for-server-mode/sequence_diagram.puml b/docs/design/low-overhead-deployment-for-server-mode/sequence_diagram.puml index c522c2087..1e443aab2 100644 --- a/docs/design/low-overhead-deployment-for-server-mode/sequence_diagram.puml +++ b/docs/design/low-overhead-deployment-for-server-mode/sequence_diagram.puml @@ -11,8 +11,8 @@ alt Startup admin ->> runner: Start runner ->> cfg_1: Read\nglobal\nconfiguration cfg_1 ->> runner: Global\nconfiguration -runner ->> cfg_2: Generate\nLlama\nStack\nconfiguration -runner ->> ls_runner: Start\nLlama Stack +runner ->> cfg_2: Generate\nOGX\nconfiguration +runner ->> ls_runner: Start\nOGX ls_runner ->> ls_service: Startup ls_service ->> ls_runner: Status + PID ls_runner ->> runner: Status + PID diff --git a/docs/design/low-overhead-deployment-for-server-mode/sequence_diagram.svg b/docs/design/low-overhead-deployment-for-server-mode/sequence_diagram.svg index 89b23ada0..c338cd4fa 100644 --- a/docs/design/low-overhead-deployment-for-server-mode/sequence_diagram.svg +++ b/docs/design/low-overhead-deployment-for-server-mode/sequence_diagram.svg @@ -42,17 +42,17 @@ Lightspeed core service - Llama Stack - runner + OGX + runner - Llama Stack - runner + OGX + runner - Llama Stack - service + OGX + service - Llama Stack - service + OGX + service alt @@ -76,14 +76,13 @@ Generate - Llama - Stack - configuration + OGX + configuration Start - Llama Stack + OGX @@ -127,7 +126,7 @@ - Stop Llama Stack + Stop OGX diff --git a/docs/design/ogx-config-merge/ogx-config-merge-spike.md b/docs/design/ogx-config-merge/ogx-config-merge-spike.md index d0a41fc12..cea25facb 100644 --- a/docs/design/ogx-config-merge/ogx-config-merge-spike.md +++ b/docs/design/ogx-config-merge/ogx-config-merge-spike.md @@ -9,6 +9,11 @@ This split increases the chance of misconfiguration, makes downstream deployment templates larger, and forces every Lightspeed team to understand OGX's internal schema. LCORE-836 asks for a single source of truth. +**Naming note:** Early drafts and the PoC used `llama_stack` / +`ogx.config`. The shipped implementation uses `ogx` / +`ogx.config`; `llama_stack` remains accepted as a deprecated YAML-section +alias (`accept_llama_stack_section_alias` in `src/models/config.py`). + **The recommendation**: A layered approach — Option C (high-level keys + `native_override` escape hatch) as the base structure, with Option D (profiles) enabled as an optional layer on top. See @@ -16,7 +21,7 @@ OGX's internal schema. LCORE-836 asks for a single source of truth. option and [Design alternatives considered](#design-alternatives-considered) for the scoring. -- **High-level keys** in `lightspeed-stack.yaml` under a new `llama_stack.config` +- **High-level keys** in `lightspeed-stack.yaml` under a new `ogx.config` section (inference, later storage/safety/...). Most downstream teams write only these. - **`native_override`** escape hatch under the same section — raw OGX @@ -27,15 +32,15 @@ for the scoring. beyond one or two reference examples under `examples/profiles/`. - **`baseline: default | empty`** selects whether the synthesis starts from LCORE's built-in baseline or a blank slate. -- **Legacy mode preserved**: existing `llama_stack.library_client_config_path` +- **Legacy mode preserved**: existing `ogx.library_client_config_path` works unchanged through a deprecation window. Mutual exclusion with the new - `llama_stack.config` block is enforced at load time. + `ogx.config` block is enforced at load time. - **Migration tool**: `lightspeed-stack --migrate-config` produces a unified single-file config from an existing (`run.yaml` + `lightspeed-stack.yaml`) pair, lossless round-trip. **PoC validation**: A library-mode PoC proves the mechanism end-to-end. -A unified `lightspeed-stack.yaml` containing only `llama_stack.config` +A unified `lightspeed-stack.yaml` containing only `ogx.config` (no external `run.yaml`) successfully drives LCORE: liveness/readiness green, `/v1/query` returns a real model response, `native_override` demonstrably takes effect. Full unit-test suite passes @@ -49,10 +54,10 @@ library-mode PoC and unit tests. ## Design options A–E -- **A (Embedded native)** — `llama_stack.config` is the raw OGX +- **A (Embedded native)** — `ogx.config` is the raw OGX schema, verbatim. Same surface area downstream teams see today, just moved into one file. No abstraction win. -- **B (High-level only)** — `llama_stack.config` exposes only LCORE-defined +- **B (High-level only)** — `ogx.config` exposes only LCORE-defined high-level keys (e.g. `inference.providers`). Best UX when every operator intent maps cleanly; painful at the edges where the high-level schema doesn't yet cover a need (no escape hatch). @@ -60,7 +65,7 @@ library-mode PoC and unit tests. a raw-LS `native_override` block deep-merged last as an escape hatch. Combines B's UX with A's flexibility. **Recommended (Decision S1).** - **D (Profiles)** — a user-authored YAML file pointed to by - `llama_stack.config.profile: `, used as the synthesis baseline + `ogx.config.profile: `, used as the synthesis baseline instead of LCORE's built-in default. A composable *layer* on top of A/B/C, not a standalone shape. LCORE ships the mechanism; downstream teams (or operators) author the YAML. @@ -83,7 +88,7 @@ for the scoring. | Option | Standalone shape | |---|---| -| A (Embedded native) | `llama_stack.config` is raw LS schema, verbatim | +| A (Embedded native) | `ogx.config` is raw LS schema, verbatim | | B (High-level only) | LCORE-defined high-level keys; no escape hatch | | **C (B + `native_override`)** | High-level keys + raw-LS escape hatch | | E (Kustomize-style patches) | Default baseline + JSON-Patch-like overlays | @@ -140,24 +145,24 @@ above pulled in, this spike's JIRAs grow accordingly. **Context**: S1 places the unified config's high-level keys (`inference.providers` today; later `rag.providers`, etc.) inside the -LS-specific subtree at `llama_stack.config.inference`. LCORE will migrate +OGX-specific subtree at `ogx.config.inference`. LCORE will migrate from OGX to Pydantic AI over time. Under S1's layout, that transition would force every downstream team to relearn the config schema — -the `llama_stack` subtree name becomes a lie, and high-level keys would +the nested backend subtree would become misleading, and high-level keys would have to move. **Recommendation**: lift the backend-agnostic keys to the top level of -`lightspeed-stack.yaml` now. Leave LS-specific knobs under -`llama_stack.config`. Extends S1; does not replace it (Option C + optional +`lightspeed-stack.yaml` now. Leave OGX-specific knobs under +`ogx.config`. Extends S1; does not replace it (Option C + optional D recommendation stands). | Today (per S1) | Proposed | |---|---| -| `llama_stack.config.inference.providers: …` | `inference.providers: …` | -| `llama_stack.config.native_override: …` | unchanged — LS-specific | -| `llama_stack.config.profile: …` | unchanged — LS-specific (points at LS run.yaml shape) | -| `llama_stack.config.baseline: …` | unchanged — LS-specific | -| Future RAG / safety / vector_io / shield high-level keys | stay under `llama_stack.config` — Pydantic AI has no equivalent abstraction (see "Pydantic AI research findings" below) | +| `ogx.config.inference.providers: …` | `inference.providers: …` | +| `ogx.config.native_override: …` | unchanged — OGX-specific | +| `ogx.config.profile: …` | unchanged — OGX-specific (points at OGX run.yaml shape) | +| `ogx.config.baseline: …` | unchanged — OGX-specific | +| Future RAG / safety / vector_io / shield high-level keys | stay under `ogx.config` — Pydantic AI has no equivalent abstraction (see "Pydantic AI research findings" below) | The synthesizer reads `inference.providers` from the top level and emits LS provider entries exactly as today — only the input node moves. When the @@ -173,22 +178,22 @@ for query-time routing). Rather than add a competing top-level key, S5 **extends that existing section** with a `providers:` list. So `inference.providers` is the high-level synthesis input, while `inference.default_model` / `default_provider` keep their current -meaning. (The PoC had this list under `llama_stack.config.inference`; +meaning. (The PoC had this list under `ogx.config.inference`; this decision is what moves it.) **Mode-detection knock-on.** Because the `inference:` section always exists (it carries defaults), unified mode is signalled by `inference.providers` being **non-empty** — or by the presence of -`llama_stack.config` — not by the section merely existing. This expands -Decision T1's shape rule from "`llama_stack.config` present" to "any +`ogx.config` — not by the section merely existing. This expands +Decision T1's shape rule from "`ogx.config` present" to "any *synthesis input* present"; see the spec doc's "Mode detection" table. -**Scope discipline — what stays under `llama_stack.config`**: anything +**Scope discipline — what stays under `ogx.config`**: anything whose vocabulary is genuinely LS-specific and unlikely to translate across backends. Today that's `native_override`, `profile`, `baseline`. The research pass (see below) confirms that RAG, safety/shields, vector storage, and the `apis` / `registered_resources` / `storage` blocks should -also stay under `llama_stack.config` whenever they ship as high-level keys +also stay under `ogx.config` whenever they ship as high-level keys — Pydantic AI has **no equivalent built-in abstraction** for any of these. **On the `inference.providers[].type` vocabulary**: keep LCORE's existing @@ -212,7 +217,7 @@ pass dated 2026-05-20 against `pydantic-ai 1.98.0`): OpenAI SDK for embeddings; there is no `pydantic_ai.vector_store` module and no public roadmap signal one is coming in the next 6–12 months. **Do not preemptively abstract `rag.*`** — keep any future - high-level RAG keys under `llama_stack.config`. Researcher confidence + high-level RAG keys under `ogx.config`. Researcher confidence it would survive a cutover today: ~25%. - Pydantic AI ships **no built-in safety / shield abstraction**. `pydantic-ai` Issue #1197 ("Guardrails") is open with no merge @@ -220,7 +225,7 @@ pass dated 2026-05-20 against `pydantic-ai 1.98.0`): (`pydantic-ai-shields`, `pydantic-ai-guardrails`) exist but have incompatible vocabularies with each other and with Llama Guard. **Do not preemptively abstract `safety.*`** — keep any future high-level - safety keys under `llama_stack.config`. Researcher confidence on + safety keys under `ogx.config`. Researcher confidence on survival: ~20%. - MCP endpoints are the one tool-runtime concept worth abstracting later (~60% confidence): both backends support MCP natively and the @@ -239,7 +244,7 @@ the inherent risk that the per-Agent model the researcher described forces LCORE's synthesizer to do more work than expected. Both manageable. **Implementation impact**: if adopted, this changes the scope of the -existing **Unified `llama_stack.config` schema + synthesizer** JIRA — it +existing **Unified `ogx.config` schema + synthesizer** JIRA — it ships the top-level shape from day one. No new JIRA is needed. --- @@ -255,7 +260,7 @@ How does LCORE tell unified-mode configs from legacy-mode configs? | Option | Works by | |---|---| -| Shape only | Presence of `llama_stack.config` → unified; else legacy | +| Shape only | Presence of `ogx.config` → unified; else legacy | | Version field only | Explicit `config_format_version: 2` required | | **Both (soft-coupled)** | Shape decides; version field optional but must agree when present | @@ -266,13 +271,13 @@ version field today. Confidence: 75%. **S5 knock-on**: since Decision S5 lifts the high-level `inference` section to the top level, the detected "shape" is the presence of any *synthesis input* — top-level `inference.providers` (non-empty) **or** -`llama_stack.config` — not just `llama_stack.config`. The soft-coupled +`ogx.config` — not just `ogx.config`. The soft-coupled version-field stance is unchanged. See the spec doc's "Mode detection" table for the full combination matrix. ### Decision T2: Override precedence (inside Option C) -When `llama_stack.config.native_override` overlaps with a high-level key, +When `ogx.config.native_override` overlaps with a high-level key, what semantics? | Strategy | Example: `safety: {excluded_categories: [a, b]}` vs override `{excluded_categories: [c]}` | @@ -308,7 +313,7 @@ Where the synthesized `run.yaml` goes at runtime: | Option | Path | |---|---| -| Temp file | `$TMPDIR/llama_stack_synthesized_config.yaml` | +| Temp file | `./.generated/run.yaml (`DEFAULT_SYNTHESIZED_CONFIG_PATH`)` | | **Persistent known path** | Local: `./.generated/run.yaml` or `~/.local/state/lightspeed-stack/run.yaml`; Container: `/app-root/.generated/run.yaml`. Overwrite on each boot. | **Recommendation**: **persistent known path, overwrite on boot**. Debuggable, @@ -360,7 +365,7 @@ caller pick the synthesis starting point. that `native_override` is the only thing the synthesizer sees. **Recommendation**: **accept this field, with `default` as the default value**. -That preserves the zero-config "fresh user authors `llama_stack.config` and +That preserves the zero-config "fresh user authors `ogx.config` and gets a working LS baseline" UX; the migration tool sets `baseline: empty` explicitly so the migrate-then-synthesize loop above matches the original `run.yaml`. Alternatives (`inherit_defaults: bool`, `starting_point: ...`) @@ -399,7 +404,7 @@ server-mode wiring, and the legacy deprecation warning. **Goals**: - A unified `lightspeed-stack.yaml` (a top-level `inference.providers` - section and/or a `llama_stack.config` block) drives LCORE in both + section and/or a `ogx.config` block) drives LCORE in both library and server modes. - A lossless dumb-mode migration tool converts the legacy two-file pair. - Legacy mode keeps working with a startup deprecation WARN through the @@ -408,7 +413,7 @@ server-mode wiring, and the legacy deprecation warning. **Scope**: - In: `UnifiedInferenceProvider` + `InferenceConfiguration.providers`, - `UnifiedLlamaStackConfig`, the synthesizer, `--migrate-config`, the LS + `UnifiedOgxConfig`, the synthesizer, `--migrate-config`, the LS container entrypoint + deployment artifacts, the deprecation WARN. - Out: smart migration factoring; high-level sections beyond `inference` (`rag` / `safety` stay backend-specific for now); LS process @@ -417,13 +422,13 @@ server-mode wiring, and the legacy deprecation warning. -#### LCORE-2336: Unified `llama_stack.config` schema + synthesizer +#### LCORE-2336: Unified `ogx.config` schema + synthesizer **Description**: Implement the unified-mode config schema and the synthesizer that produces a full OGX `run.yaml` from it. The high-level `providers` list lives on the existing top-level `InferenceConfiguration` (`inference.providers`) — backend-agnostic, so -it survives a future backend change — and `UnifiedLlamaStackConfig` +it survives a future backend change — and `UnifiedOgxConfig` holds only the backend-specific knobs (`baseline` / `profile` / `native_override`). Wire library mode to the synthesizer. Preserve legacy mode through mutual-exclusion validation on the root @@ -437,21 +442,21 @@ configuration model. (Full design: the spec doc.) `providers: list[UnifiedInferenceProvider]` (default empty) — this is the high-level synthesis input; `default_model` / `default_provider` keep their current query-routing meaning. - - Add `UnifiedLlamaStackConfig` (`baseline` / `profile` / - `native_override`) and a `config` field on `LlamaStackConfiguration`. + - Add `UnifiedOgxConfig` (`baseline` / `profile` / + `native_override`) and a `config` field on `OgxConfiguration`. - Add the unified-vs-legacy `@model_validator` to the **root** `Configuration` model (it spans top-level `inference.providers` and - `llama_stack.*`). + `ogx.*`). - New functions in `src/ogx_configuration.py`: `synthesize_configuration`, `deep_merge_list_replace`, `apply_high_level_inference`, `load_default_baseline`, `synthesize_to_file`. - A shipped default baseline at `src/data/default_run.yaml`. -- Library-mode wiring in `src/client.py`: detect unified vs legacy +- Library-mode wiring in `src/client/ogx.py`: detect unified vs legacy (synthesis input present vs `library_client_config_path`), write synthesized file, pass path to library client. - Cross-field validation: reject a synthesis input (`inference.providers` non-empty, or `config`) set together with `library_client_config_path`. -- Legacy behavior (`llama_stack.library_client_config_path` path) unchanged. +- Legacy behavior (`ogx.library_client_config_path` path) unchanged. **Acceptance criteria**: @@ -467,10 +472,10 @@ configuration model. (Full design: the spec doc.) Read the "Architecture" and "Implementation Suggestions" sections of docs/design/ogx-config-merge/ogx-config-merge.md. Key files to create or modify: - src/models/config.py (new classes; modify LlamaStackConfiguration) + src/models/config.py (new classes; modify OgxConfiguration) src/ogx_configuration.py (synthesize_configuration + helpers) src/data/default_run.yaml (new) - src/client.py (library-mode wiring) + src/client/ogx.py (library-mode wiring) To verify: run a unified-mode config end-to-end via `uv run lightspeed-stack -c ` and confirm /v1/query succeeds. ``` @@ -481,7 +486,7 @@ To verify: run a unified-mode config end-to-end via `uv run lightspeed-stack -c **Description**: Implement `--migrate-config` on the `lightspeed-stack` CLI that produces a unified single-file config from an existing (`run.yaml` + `lightspeed-stack.yaml`) pair. Dumb mode places the entire -`run.yaml` body under `llama_stack.config.native_override` with +`run.yaml` body under `ogx.config.native_override` with `baseline: empty`, removes `library_client_config_path`. **Scope**: @@ -505,7 +510,7 @@ that produces a unified single-file config from an existing ```text Read "Migration / backwards compatibility" and "Appendix A — Worked example: legacy → unified migration" in docs/design/ogx-config-merge/ogx-config-merge.md. Key files: src/lightspeed_stack.py, src/ogx_configuration.py, -tests/unit/test_llama_stack_synthesize.py. +tests/unit/test_ogx_synthesize.py. To verify: migrate the repo's root run.yaml + lightspeed-stack.yaml, then start LCORE with the output; confirm /v1/query works. ``` @@ -555,8 +560,8 @@ to the migration doc. Legacy mode continues to fully function. **Scope**: -- Warning emission point: on load in `LlamaStackConfiguration` - `check_llama_stack_model` validator, or at LCORE startup. +- Warning emission point: on load in `OgxConfiguration` + `check_ogx_model` validator, or at LCORE startup. - Log line format includes a stable URL fragment to the migration doc. **Acceptance criteria**: @@ -822,7 +827,7 @@ To verify: rendered docs present the unified mode first; legacy mode is visibly files — one remote-provider (OpenAI) and one inline-provider (sentence- transformers + FAISS) — purely as reference material. Document how operators write and reference their own profiles via -`llama_stack.config.profile: `. +`ogx.config.profile: `. **Scope**: @@ -955,8 +960,8 @@ Two files: - **`lightspeed-stack.yaml`** — LCORE settings: service host/port, auth, conversation cache, user data collection, MCP servers, authentication, - authorization, quota, etc. Also contains `llama_stack:` with - connection-to-LS settings (URL/api_key or library-client mode with a path + authorization, quota, etc. Also contains `ogx:` with + connection-to-OGX settings (URL/api_key or library-client mode with a path to an external `run.yaml`). - **`run.yaml`** — OGX operational config: `apis`, `providers` (inference, safety, tool_runtime, vector_io, agents, ...), `storage`, @@ -969,7 +974,7 @@ Two files: BYOK RAG entries, Solr/OKP provider/store/model registration. Output is an enriched `run.yaml`. - Called in two places: `scripts/ogx-entrypoint.sh` at LS container - boot (server mode) and `src/client.py:_enrich_library_config()` (library + boot (server mode) and `src/client/ogx.py:_enrich_library_config()` (library mode). - LCORE-779 made this automatic; LCORE-518 (closed spike) proved (re)generation feasibility. Both are the groundwork the current spike builds on. @@ -1107,7 +1112,7 @@ initializes, serves. One process. **Server mode**: OGX runs as a separate process (container). LCORE connects to it over HTTP. Under unified mode, the LS container's entrypoint reads the mounted `lightspeed-stack.yaml`, the Python CLI auto-detects -unified mode, synthesizes `run.yaml`, then `exec llama stack run` with it. +unified mode, synthesizes `run.yaml`, then `exec ogx stack run` with it. LCORE container reads the same `lightspeed-stack.yaml`, ignores the `config` sub-block (server mode — only connection fields matter), connects. Two processes. LCORE does **not** start, monitor, or supervise the LS @@ -1130,8 +1135,8 @@ Detection rule at load time: | `lightspeed-stack.yaml` shape | Interpretation | |---|---| -| `llama_stack.library_client_config_path` set, no `llama_stack.config` | **Legacy** — today's behavior | -| `llama_stack.config.*` present | **Unified** — new path | +| `ogx.library_client_config_path` set, no `ogx.config` | **Legacy** — today's behavior | +| `ogx.config.*` present | **Unified** — new path | | Both present | Error at load time — clear message | | Neither (remote URL only, no config) | Existing remote mode — unchanged | @@ -1151,14 +1156,14 @@ Relative to `upstream/main`: | File | Purpose | |---|---| -| `src/models/config.py` | New classes: `UnifiedInferenceProvider`, `UnifiedInferenceSection`, `UnifiedLlamaStackConfig`; modified `LlamaStackConfiguration` (adds `config` field + mutual-exclusion validator). _PoC layout; the implementation follows Decision S5 — `inference.providers` on the top-level `InferenceConfiguration`, validator on the root `Configuration` model, no `UnifiedInferenceSection` (see the schema JIRA)._ | +| `src/models/config.py` | New classes: `UnifiedInferenceProvider`, `UnifiedInferenceSection`, `UnifiedOgxConfig`; modified `OgxConfiguration` (adds `config` field + mutual-exclusion validator). _PoC layout; the implementation follows Decision S5 — `inference.providers` on the top-level `InferenceConfiguration`, validator on the root `Configuration` model, no `UnifiedInferenceSection` (see the schema JIRA)._ | | `src/ogx_configuration.py` | New: `synthesize_configuration`, `deep_merge_list_replace`, `apply_high_level_inference`, `load_default_baseline`, `synthesize_to_file`, `migrate_config_dumb`. CLI `main()` auto-detects unified vs legacy. | | `src/data/default_run.yaml` | Built-in default baseline (copied from repo root `run.yaml` for the PoC — implementation JIRA should slim it down; see PoC surprise about `EXTERNAL_PROVIDERS_DIR`) | -| `src/client.py` | Library-mode path picks synthesis for unified configs, enrichment for legacy | +| `src/client/ogx.py` | Library-mode path picks synthesis for unified configs, enrichment for legacy | | `src/lightspeed_stack.py` | `--migrate-config`, `--run-yaml`, `--migrate-output` flags | | `scripts/ogx-entrypoint.sh` | Comment updated — script itself needs no change (Python CLI auto-detects) | | `test.containerfile` | Copies `src/data/` into the LS container | -| `tests/unit/test_llama_stack_synthesize.py` | 22 new tests: merge semantics, high-level inference, synthesize pipeline, migration round-trip | +| `tests/unit/test_ogx_synthesize.py` | 22 new tests: merge semantics, high-level inference, synthesize pipeline, migration round-trip | | `tests/unit/models/config/test_ogx_configuration.py` | 3 new tests: unified/legacy mutual exclusion | | `tests/unit/models/config/test_dump_configuration.py` | 5 expected-dict updates (new `config: None` field appears in dumps) | | `tests/unit/test_client.py` | Error-message regex updated | @@ -1191,5 +1196,5 @@ curl -s -X POST http://localhost:8080/v1/query \ -d '{"query": "Name three primary colors. One sentence."}' # 3. Inspect what was synthesized -cat /tmp/llama_stack_synthesized_config.yaml +cat ./.generated/run.yaml ``` diff --git a/docs/design/ogx-config-merge/ogx-config-merge.md b/docs/design/ogx-config-merge/ogx-config-merge.md index 4a63bc48b..b0e7b2d8c 100644 --- a/docs/design/ogx-config-merge/ogx-config-merge.md +++ b/docs/design/ogx-config-merge/ogx-config-merge.md @@ -3,7 +3,7 @@ | | | |--------------------|----------------------------------------------------------------------------------| | **Date** | 2026-04-23 | -| **Component** | Lightspeed Core Stack (src/models/config.py, src/ogx_configuration.py, src/client.py, src/lightspeed_stack.py, scripts/ogx-entrypoint.sh) | +| **Component** | Lightspeed Core Stack (src/models/config.py, src/ogx_configuration.py, src/client/ogx.py, src/lightspeed_stack.py, scripts/ogx-entrypoint.sh) | | **Authors** | Maxim Svistunov | | **Feature** | [LCORE-836](https://redhat.atlassian.net/browse/LCORE-836) | | **Spike** | [ogx-config-merge-spike.md](ogx-config-merge-spike.md) | @@ -16,7 +16,7 @@ This feature collapses the two Lightspeed Core configuration files — operational config) — into a single `lightspeed-stack.yaml`. At runtime, LCORE synthesizes a full OGX `run.yaml` from high-level operator-facing inputs (a top-level `inference.providers` list, plus a -`llama_stack.config` sub-section) and hands it to OGX (library +`ogx.config` sub-section) and hands it to OGX (library client or subprocess, mode-dependent). Key shape: @@ -24,23 +24,24 @@ Key shape: - **Top-level high-level sections** for the common path. v1 ships `inference.providers` — added to the *existing* top-level `inference:` section (alongside its `default_model` / `default_provider`). These - sit at the root of `lightspeed-stack.yaml`, not under `llama_stack`, + sit at the root of `lightspeed-stack.yaml`, not under `ogx`, so they survive a future backend change (Decision S5 in the spike). Future high-level sections (`rag`, `safety`, …) stay under - `llama_stack.config` until proven backend-agnostic. -- `llama_stack.config.native_override` escape hatch — raw OGX + `ogx.config` until proven backend-agnostic. +- `ogx.config.native_override` escape hatch — raw OGX schema, deep-merged with list replacement. Covers anything the high-level sections don't express. -- `llama_stack.config.profile` — path to a user-authored YAML that serves +- `ogx.config.profile` — path to a user-authored YAML that serves as the synthesis baseline. -- `llama_stack.config.baseline: default | byo-llm | empty` — pick +- `ogx.config.baseline: default | byo-llm | empty` — pick LCORE's built-in baseline (includes a conditional OpenAI provider), the same baseline without that OpenAI row, or an empty dict (used by the migration tool for exact round-trip). -- Legacy two-file mode (`llama_stack.library_client_config_path` + +- Legacy two-file mode (`ogx.library_client_config_path`; the deprecated + `llama_stack` YAML-section alias is still accepted) + external `run.yaml`) is preserved during a deprecation window; mutually exclusive with the unified *synthesis inputs* (a non-empty - `inference.providers` or a `llama_stack.config` block). + `inference.providers` or a `ogx.config` block). ## Why @@ -64,16 +65,16 @@ detail that LCORE owns, not an operator-facing artifact. ## Requirements - **R1:** `lightspeed-stack.yaml` using the unified schema (a non-empty - top-level `inference.providers`, and/or a `llama_stack.config` + top-level `inference.providers`, and/or a `ogx.config` sub-section) and no external `run.yaml` boots LCORE in both library and server modes and serves `/v1/query` successfully. -- **R2:** Legacy mode (`llama_stack.library_client_config_path` + +- **R2:** Legacy mode (`ogx.library_client_config_path` + external `run.yaml`) works unchanged through the deprecation window: fully functional with a startup deprecation WARN in 0.6 and 0.7, removed in 0.8 (Decision S2, confirmed 2026-05-20, schedule revised 2026-08-24). -- **R3:** Setting both `llama_stack.config` and - `llama_stack.library_client_config_path` in the same file fails at +- **R3:** Setting both `ogx.config` and + `ogx.library_client_config_path` in the same file fails at configuration load time with a clear error message pointing to the migration tool. - **R4:** `lightspeed-stack --migrate-config --run-yaml X -c Y @@ -81,7 +82,7 @@ detail that LCORE owns, not an operator-facing artifact. two-file pair. Running the migrated file drives OGX to byte-identical behavior as the original pair (dumb-mode lossless round-trip). -- **R5:** When `llama_stack.config.native_override` overlaps a key set +- **R5:** When `ogx.config.native_override` overlaps a key set by the high-level section or by the baseline, deep-merge semantics apply with list replacement (maps merge recursively; lists are replaced wholesale; scalars are replaced). The override wins over the @@ -103,8 +104,8 @@ detail that LCORE owns, not an operator-facing artifact. - **R9:** The unified schema (a) extends the existing top-level `InferenceConfiguration` with a `providers: list[UnifiedInferenceProvider]` field and (b) adds a - `config: Optional[UnifiedLlamaStackConfig]` field to - `LlamaStackConfiguration` (holding `baseline` / `profile` / + `config: Optional[UnifiedOgxConfig]` field to + `OgxConfiguration` (holding `baseline` / `profile` / `native_override`). Cross-field validation on the **root** `Configuration` model enforces mutual exclusion between the unified synthesis inputs and legacy mode, and all unified-mode models reject @@ -118,7 +119,7 @@ detail that LCORE owns, not an operator-facing artifact. location for debugging. - **R11:** Shape detection determines mode. Unified mode is signalled by the presence of any *synthesis input* — a non-empty top-level - `inference.providers` or a `llama_stack.config` block; legacy mode by + `inference.providers` or a `ogx.config` block; legacy mode by `library_client_config_path`. An optional `config_format_version` field is accepted but must agree with the detected shape when present. See the "Mode detection" table under Architecture for the full matrix. @@ -150,7 +151,7 @@ files — authors read it to write Gherkin scenarios. | Req | Observable behavior | Verified by | |---|---|---| -| R1 | Unified config (top-level `inference.providers` and/or `llama_stack.config`, no external `run.yaml`) boots LCORE in library and server mode; `/liveness`, `/readiness`, `/v1/query` succeed | e2e | +| R1 | Unified config (top-level `inference.providers` and/or `ogx.config`, no external `run.yaml`) boots LCORE in library and server mode; `/liveness`, `/readiness`, `/v1/query` succeed | e2e | | R2 | Legacy two-file config still boots and serves; one startup deprecation WARN in 0.6; no WARN in unified mode | e2e | | R3 | A config with a synthesis input *and* `library_client_config_path` fails at load with an error naming `--migrate-config` (cover both the `inference.providers` and the `config` case) | e2e + unit | | R4 | `--migrate-config` on a legacy pair yields a unified file driving byte-identical LS behavior; migrate→synthesize round-trips to the original `run.yaml` | e2e + unit (round-trip) | @@ -180,7 +181,7 @@ lightspeed-stack.yaml (unified mode) ┌────────────────────────────┐ Baseline selection (profile / │ Synthesizer │ default / empty) + enrichment │ synthesize_configuration │ (BYOK RAG, Solr/OKP) + high-level - │ (llama_stack_config…) │ sections + native_override deep-merge. + │ (ogx_config…) │ sections + native_override deep-merge. └────────────┬───────────────┘ │ synthesized run.yaml (dict) ▼ @@ -189,14 +190,14 @@ lightspeed-stack.yaml (unified mode) Write to deterministic path. Written by LS container's entrypoint AsyncOGXAsLibraryClient script (same synthesizer, same CLI, reads the path and initializes. auto-detects unified via Python). - `llama stack run ` starts OGX. + `ogx stack run ` starts OGX. LCORE connects by URL. ``` ### Trigger mechanism At LCORE startup (library mode): if any synthesis input is present (a -non-empty top-level `inference.providers`, or a `llama_stack.config` +non-empty top-level `inference.providers`, or a `ogx.config` block), the synthesizer produces a `run.yaml` dict, writes it to disk, and passes the path to the library client. @@ -211,7 +212,7 @@ before. ### Mode detection *Synthesis inputs* are the top-level high-level sections (v1: a non-empty -`inference.providers`; future `rag`, …) and the `llama_stack.config` +`inference.providers`; future `rag`, …) and the `ogx.config` block. The loaded `lightspeed-stack.yaml` maps to a mode as follows: | Shape | Mode | @@ -248,12 +249,12 @@ config matches the previous unconditional openai provider. ### Configuration Top-level high-level sections plus a sub-section under the existing -`llama_stack` block: +`ogx` block: ```yaml # Top-level inference config — the existing `inference:` section, extended # with a `providers:` list (Decision S5). Backend-agnostic: it stays at the -# root, not under `llama_stack`, so it survives a future backend change. +# root, not under `ogx`, so it survives a future backend change. inference: default_model: gpt-4o-mini # existing — query-time default routing default_provider: openai # existing — query-time default routing @@ -263,7 +264,7 @@ inference: allowed_models: [gpt-4o-mini] - type: sentence_transformers -llama_stack: +ogx: use_as_library_client: true # NOTE: library_client_config_path intentionally OMITTED in unified mode. # Setting a synthesis input (`inference.providers` or `config`) together @@ -303,7 +304,7 @@ class InferenceConfiguration(ConfigurationBase): providers: list[UnifiedInferenceProvider] = Field(default_factory=list) -class UnifiedLlamaStackConfig(ConfigurationBase): +class UnifiedOgxConfig(ConfigurationBase): # Backend-specific knobs only. Per Decision S5, the backend-agnostic # high-level sections (inference, ...) live at the root, NOT here. baseline: Literal["default", "empty", "byo-llm"] = "default" @@ -311,28 +312,28 @@ class UnifiedLlamaStackConfig(ConfigurationBase): native_override: dict[str, Any] = Field(default_factory=dict) -class LlamaStackConfiguration(ConfigurationBase): +class OgxConfiguration(ConfigurationBase): # existing fields unchanged (url, api_key, use_as_library_client, # library_client_config_path, timeout) - config: Optional[UnifiedLlamaStackConfig] = None + config: Optional[UnifiedOgxConfig] = None class Configuration(ConfigurationBase): # The root lightspeed-stack.yaml model (existing). Relevant fields: inference: InferenceConfiguration = Field(default_factory=InferenceConfiguration) - llama_stack: LlamaStackConfiguration + ogx: OgxConfiguration # ... other existing fields (name, service, ...) ... @model_validator(mode="after") def check_unified_vs_legacy(self) -> Self: # Synthesis inputs span the root (inference.providers) and the - # nested llama_stack.config, so the check lives here, not on - # LlamaStackConfiguration. + # nested ogx.config, so the check lives here, not on + # OgxConfiguration. synthesis_input = ( bool(self.inference.providers) - or self.llama_stack.config is not None + or self.ogx.config is not None ) - legacy_input = self.llama_stack.library_client_config_path is not None + legacy_input = self.ogx.library_client_config_path is not None if synthesis_input and legacy_input: raise ValueError("... mutually exclusive ... use --migrate-config") # ...legacy / remote checks preserved... @@ -373,7 +374,7 @@ removed `-g/-i/-o` flags is cleaned up as part of the docs JIRA. - **Library mode with no synthesis input and no `library_client_config_path`**: raised during the same root validator. Error identifies the valid paths (populate `inference.providers` or a - `llama_stack.config` block, or set `library_client_config_path`). + `ogx.config` block, or set `library_client_config_path`). - **`profile:` path does not exist**: surfaced as `FileNotFoundError` from `open(profile_path)` during synthesis. The implementation JIRA should wrap this with context about where the path was resolved. @@ -420,7 +421,7 @@ removed `-g/-i/-o` flags is cleaned up as part of the docs JIRA. ### Migration / backwards compatibility Coexistence mechanism: shape detection (see R11). Legacy configs with -`llama_stack.library_client_config_path` continue through the +`ogx.library_client_config_path` continue through the configured deprecation window. Three operator-facing migration paths (choose per deployment): @@ -449,10 +450,10 @@ not have had a full release with a working migration path. Releases: | File | What to do | |---|---| -| `src/models/config.py` | Add `UnifiedInferenceProvider`. Extend the existing `InferenceConfiguration` with `providers: list[UnifiedInferenceProvider]`. Add `UnifiedLlamaStackConfig` (`baseline`/`profile`/`native_override`) and a `config` field on `LlamaStackConfiguration`. Put the unified-vs-legacy `model_validator` on the **root** `Configuration` model (spans `inference.providers` + `llama_stack.*`). | +| `src/models/config.py` | Add `UnifiedInferenceProvider`. Extend the existing `InferenceConfiguration` with `providers: list[UnifiedInferenceProvider]`. Add `UnifiedOgxConfig` (`baseline`/`profile`/`native_override`) and a `config` field on `OgxConfiguration`. Put the unified-vs-legacy `model_validator` on the **root** `Configuration` model (spans `inference.providers` + `ogx.*`). | | `src/ogx_configuration.py` | Add `synthesize_configuration`, `deep_merge_list_replace`, `apply_high_level_inference`, `load_default_baseline`, `synthesize_to_file`, `migrate_config_dumb`, `PROVIDER_TYPE_MAP`, `DEFAULT_BASELINE_RESOURCE`. Update `main()` to auto-detect unified vs legacy. | | `src/data/default_run.yaml` | New file — a thinner baseline than today's repo-root `run.yaml`. Notably do **not** reference `${env.EXTERNAL_PROVIDERS_DIR}` without a default (see "Findings discovered during PoC" in the spike doc). OpenAI is conditional on `OPENAI_API_KEY` (`${env.OPENAI_API_KEY:+openai}` / `${env.OPENAI_API_KEY:=}`). | -| `src/client.py` | In `_load_library_client`: branch on `config.config` presence. Add `_synthesize_library_config()` that calls the synthesizer and writes to the deterministic path (R10). Keep `_enrich_library_config` for legacy. | +| `src/client/ogx.py` | In `_load_library_client`: branch on synthesis input (`bool(app_config.inference.providers) or config.config is not None`); use `_synthesize_library_config()` for unified mode (R10) and `_enrich_library_config` for legacy (`library_client_config_path`). | | `src/lightspeed_stack.py` | Add `--migrate-config`, `--run-yaml`, `--migrate-output`, `--synthesized-config-output` flags. Add an early-exit branch in `main()` that dispatches to `migrate_config_dumb` when `--migrate-config` is set. Clean up stale docstring. | | `scripts/ogx-entrypoint.sh` | No functional change — the Python CLI already auto-detects. Update the comment to document both modes. | | `test.containerfile` | Copy `src/data/` into `/opt/app-root/data/` so `load_default_baseline()` resolves inside the LS container. | @@ -463,7 +464,7 @@ not have had a full release with a working migration path. Releases: **`synthesize_configuration` pipeline** (the core new function): 1. Resolve the backend-specific block `unified = - lcs_config["llama_stack"].get("config")` — may be `None` when the + lcs_config["ogx"].get("config")` — may be `None` when the operator set only top-level `inference.providers` (then baseline defaults to `default`, no profile, no `native_override`). 2. Baseline: if `unified` and `unified.profile` set → load that file. @@ -482,13 +483,13 @@ not have had a full release with a working migration path. Releases: 7. `dedupe_providers_vector_io` again for good measure. 8. Return the final dict. -**`_load_library_client` fork point** (in `src/client.py`). The check is +**`_load_library_client` fork point** (in `src/client/ogx.py`). The check is "is there a synthesis input?", which spans the root `inference.providers` -and `llama_stack.config`, so the client needs the root config (or a -precomputed flag) rather than only the `llama_stack` block: +and `ogx.config`, so the client needs the root config (or a +precomputed flag) rather than only the `ogx` block: ```python -# app_config is the root Configuration; ls = app_config.llama_stack +# app_config is the root Configuration; ls = app_config.ogx synthesis_input = bool(app_config.inference.providers) or ls.config is not None if synthesis_input: self._config_path = self._synthesize_library_config() @@ -504,10 +505,10 @@ All new config classes extend `ConfigurationBase` (`extra="forbid"`). Use `Field()` with defaults, title, and description for every attribute. The unified-vs-legacy mutual-exclusion check is cross-field and spans the root model's top-level `inference.providers` and the nested -`llama_stack.config` / `library_client_config_path`, so it lives as a +`ogx.config` / `library_client_config_path`, so it lives as a `@model_validator` on the **root** `Configuration` model (not on -`UnifiedLlamaStackConfig` or `LlamaStackConfiguration`). Within -`UnifiedLlamaStackConfig` no cross-field validation is needed — +`UnifiedOgxConfig` or `OgxConfiguration`). Within +`UnifiedOgxConfig` no cross-field validation is needed — synthesis precedence is ordered and handled by the synthesizer. Example config files live in `examples/profiles/` (two reference @@ -518,7 +519,7 @@ reference. ### Test patterns - Framework: pytest + pytest-mock. Unit tests live in - `tests/unit/test_llama_stack_synthesize.py` (synthesizer + migration) + `tests/unit/test_ogx_synthesize.py` (synthesizer + migration) and `tests/unit/models/config/test_ogx_configuration.py` (schema validation). - Merge semantics: parametric tests over scalar / map / list / @@ -544,7 +545,7 @@ reference. - **Additional high-level sections** beyond `inference` — `rag`, `safety`, `storage`, `tools`, `vector_stores`, etc. Add as real demand appears, not speculatively. Per Decision S5 and the Pydantic AI - research, these stay under `llama_stack.config` (not lifted to the top + research, these stay under `ogx.config` (not lifted to the top level like `inference`) until proven backend-agnostic. - **User-supplied profile directory**: `profile_dir: /etc/lcore/profiles/` with name-based lookup. Deferred to v2. @@ -594,7 +595,7 @@ providers: ```yaml # lightspeed-stack.yaml name: LCS -llama_stack: +ogx: use_as_library_client: true library_client_config_path: ./run.yaml # ... rest ... @@ -614,7 +615,7 @@ Produces: ```yaml # lightspeed-stack-unified.yaml name: LCS -llama_stack: +ogx: use_as_library_client: true # library_client_config_path is REMOVED config: @@ -642,7 +643,7 @@ high-level sections) is optional and per-deployment. ```yaml # examples/profiles/openai-remote.yaml # A minimal profile for an OpenAI-backed remote OGX. -# Referenced via `llama_stack.config.profile: examples/profiles/openai-remote.yaml`. +# Referenced via `ogx.config.profile: examples/profiles/openai-remote.yaml`. version: 2 apis: [agents, inference, safety, tool_runtime, vector_io] providers: diff --git a/docs/design/prompt-guardrails/poc-results/README.md b/docs/design/prompt-guardrails/poc-results/README.md index 149b00ddf..e02c17db2 100644 --- a/docs/design/prompt-guardrails/poc-results/README.md +++ b/docs/design/prompt-guardrails/poc-results/README.md @@ -53,7 +53,7 @@ ship, and thresholds are not the remedy (`06`). Measured on the 2B model; `guardrails:` config section (keeps the throwaway out of `Configuration` / OpenAPI). - One detector backend (`granite_guardian`); no `openai_moderations` / - `llama_stack_shields` backends. + `ogx_shields` backends. - Output check is non-streaming only; no streaming checkpoints (Decision T4). - tool_content is a post-hoc check on collected tool results, not the diff --git a/docs/design/prompt-guardrails/poc-results/lcs-poc-config.yaml b/docs/design/prompt-guardrails/poc-results/lcs-poc-config.yaml index c7424d022..e61411c38 100644 --- a/docs/design/prompt-guardrails/poc-results/lcs-poc-config.yaml +++ b/docs/design/prompt-guardrails/poc-results/lcs-poc-config.yaml @@ -7,7 +7,7 @@ service: workers: 1 color_log: true access_log: true -llama_stack: +ogx: use_as_library_client: false url: http://localhost:8321 api_key: xyzzy diff --git a/docs/design/prompt-guardrails/prompt-guardrails-spike.md b/docs/design/prompt-guardrails/prompt-guardrails-spike.md index 49e57c343..5b4cb91e9 100644 --- a/docs/design/prompt-guardrails/prompt-guardrails-spike.md +++ b/docs/design/prompt-guardrails/prompt-guardrails-spike.md @@ -99,7 +99,7 @@ backends. Ship three backends: `granite_guardian` (chat-template invocation, custom risks), `openai_moderations` (any OpenAI-compatible `/v1/moderations` endpoint — this also covers OGX 1.x's `moderation_endpoint` services and TrustyAI gateways, making D a *deployment choice*, not an architecture), and -`llama_stack_shields` (transitional bridge wrapping today's behavior). +`ogx_shields` (transitional bridge wrapping today's behavior). The existing input-moderation path keeps working unchanged during the transition (see [S5](#decision-s5-fate-of-the-existing-shields-moderation-path)). @@ -250,7 +250,7 @@ with `shield_ids` request-override semantics documented in | B — Replace immediately | Migrate the input path onto the new layer in this epic; remove the shields code. | **Recommendation**: **A** — additive now, deprecation decision deferred to -the LCORE-1099 work. The `llama_stack_shields` detector backend gives +the LCORE-1099 work. The `ogx_shields` detector backend gives deployments a config-level migration path in the meantime. No behavior change for existing deployments. @@ -307,12 +307,12 @@ _No answer needed — this will be implemented as recommended unless you object. | Option | Description | |--------|-------------| -| A — Protocol + per-type adapters | `DetectorBackend` protocol (`async check(content, rule) -> DetectionResult`); adapters: `granite_guardian`, `openai_moderations`, `llama_stack_shields`. | +| A — Protocol + per-type adapters | `DetectorBackend` protocol (`async check(content, rule) -> DetectionResult`); adapters: `granite_guardian`, `openai_moderations`, `ogx_shields`. | | B — Single Guardian-only implementation | Simpler; closes the door on OGX 1.x moderation endpoints and TrustyAI gateways. | **Recommendation**: **A**. Guardian invocation is OpenAI chat-completions with the risk selected via the guardian chat template (system slot); -`openai_moderations` maps categories to rules; `llama_stack_shields` wraps +`openai_moderations` maps categories to rules; `ogx_shields` wraps the existing `run_shield_moderation` behavior. **Confidence**: 85% @@ -812,7 +812,7 @@ runner executing a point's rules in parallel, and a hook in - Activation via `LCS_GUARDRAILS_POC_CONFIG` env var, not the `guardrails:` config section (avoids touching `Configuration`/OpenAPI in a throwaway). - Single detector (Granite Guardian via OpenAI-compatible endpoint); no - `openai_moderations` / `llama_stack_shields` backends. + `openai_moderations` / `ogx_shields` backends. - Tool-content check is post-hoc on collected tool results (Decision T5 option B), not the gating capability hook (option A). - Output check is non-streaming `/v1/query` only; no streaming checkpoints. diff --git a/docs/design/prompt-guardrails/prompt-guardrails.md b/docs/design/prompt-guardrails/prompt-guardrails.md index 7b70d49e9..cbe86b33e 100644 --- a/docs/design/prompt-guardrails/prompt-guardrails.md +++ b/docs/design/prompt-guardrails/prompt-guardrails.md @@ -165,7 +165,7 @@ see Open Questions.) guardrails: detectors: - name: guardian - type: granite_guardian # granite_guardian | openai_moderations | llama_stack_shields + type: granite_guardian # granite_guardian | openai_moderations | ogx_shields url: http://vllm.example:8000/v1 model: ibm-granite/granite-guardian-3.3-8b api_key_path: /run/secrets/guardian-key # optional @@ -234,7 +234,7 @@ call detection the same way (R7a depends on it). Backends: - **openai_moderations** — POST `/v1/moderations`; a rule maps to flagged categories (all, or a configured subset). Covers OGX 1.x `moderation_endpoint` services, TrustyAI gateways, and OpenAI itself. -- **llama_stack_shields** — transitional bridge delegating to the existing +- **ogx_shields** — transitional bridge delegating to the existing `client.moderations.create` OGX shields path, easing config-level migration (spike Decision S5). @@ -298,7 +298,7 @@ metric label; `allow` logs a warning and proceeds. Config errors No `guardrails:` section ⇒ byte-identical behavior to today (R11). The OGX shields path is untouched; its deprecation is deferred to the -OGX 1.x migration (LCORE-1099). The `llama_stack_shields` backend lets +OGX 1.x migration (LCORE-1099). The `ogx_shields` backend lets deployments move their config to the new schema before OGX migrates. ## Acceptance test surface diff --git a/docs/devel_doc/ARCHITECTURE.md b/docs/devel_doc/ARCHITECTURE.md index c20752f08..a1e2815aa 100644 --- a/docs/devel_doc/ARCHITECTURE.md +++ b/docs/devel_doc/ARCHITECTURE.md @@ -137,7 +137,7 @@ LCore requires two main configuration files: 2. **OGX Configuration** (`run.yaml`): - Required for both library and server modes - Defines LLM providers, models, RAG stores, shields - - See [OGX documentation](https://llama-stack.readthedocs.io/) for details + - See [OGX documentation](https://ogx-ai.github.io/) for details **Configuration Validation:** - Pydantic models validate configuration structure at startup diff --git a/docs/devel_doc/architecture.svg b/docs/devel_doc/architecture.svg index 6d9b53acd..e67e10268 100644 --- a/docs/devel_doc/architecture.svg +++ b/docs/devel_doc/architecture.svg @@ -1,7 +1,7 @@ - + @@ -285,14 +285,14 @@
-
Llama Stack
+
OGX
(library or
service mode)
- Llama Stack... + OGX @@ -722,11 +722,11 @@
-
Llama Stack
Configuration
+
OGX
Configuration
- Llama Stack... + OGX Configuration diff --git a/docs/devel_doc/container_orchestration.md b/docs/devel_doc/container_orchestration.md index 7a12bbb68..9d695cc01 100644 --- a/docs/devel_doc/container_orchestration.md +++ b/docs/devel_doc/container_orchestration.md @@ -207,7 +207,7 @@ Override any of these variables when running `make`: ```bash make run OGX_PORT=9321 ``` -*Note: Also update `llama_stack.url` in `lightspeed-stack.yaml` to `http://localhost:9321`* +*Note: Also update `ogx.url` in `lightspeed-stack.yaml` to `http://localhost:9321`* **Use custom config files:** ```bash @@ -216,7 +216,7 @@ make run CONFIG=my-config.yaml OGX_CONFIG=my-run.yaml **Use custom container name:** ```bash -make run OGX_CONTAINER_NAME=my-llama-stack +make run OGX_CONTAINER_NAME=my-ogx ``` **Force Docker instead of Podman:** @@ -245,7 +245,7 @@ This file configures the Lightspeed Core Stack service. **OGX connection settings:** ```yaml -llama_stack: +ogx: use_as_library_client: false url: http://localhost:8321 # api_key: custom-key # Optional authentication @@ -497,7 +497,7 @@ Error: cannot listen on the TCP port: listen tcp4 0.0.0.0:8321: bind: address al Don't forget to update `lightspeed-stack.yaml`: ```yaml - llama_stack: + ogx: url: http://localhost:9321 ``` @@ -579,7 +579,7 @@ curl -fsSL https://get.docker.com | sh 1. **Check OGX URL in config:** ```yaml # lightspeed-stack.yaml - llama_stack: + ogx: url: http://localhost:8321 # Must match OGX_PORT ``` @@ -734,52 +734,52 @@ If you need more control than the Makefile provides, you can manage the containe #### Build the Image ```bash -podman build -f deploy/ogx/test.containerfile -t my-llama-stack:custom . +podman build -f deploy/ogx/test.containerfile -t my-ogx:custom . ``` #### Run the Container ```bash podman run -d \ - --name my-llama-stack \ + --name my-ogx \ -p 9000:8321 \ -v $(pwd)/run.yaml:/opt/app-root/run.yaml:z \ -v $(pwd)/lightspeed-stack.yaml:/opt/app-root/lightspeed-stack.yaml:ro,z \ -e OPENAI_API_KEY \ - my-llama-stack:custom + my-ogx:custom ``` #### Monitor the Container ```bash # Follow logs -podman logs -f my-llama-stack +podman logs -f my-ogx # Check health -podman inspect --format='{{.State.Health.Status}}' my-llama-stack +podman inspect --format='{{.State.Health.Status}}' my-ogx # Execute commands inside container -podman exec my-llama-stack curl http://localhost:8321/v1/health +podman exec my-ogx curl http://localhost:8321/v1/health # View container stats (CPU, memory) -podman stats my-llama-stack +podman stats my-ogx ``` #### Stop and Remove ```bash # Stop gracefully -podman stop -t 10 my-llama-stack +podman stop -t 10 my-ogx # Remove container -podman rm my-llama-stack +podman rm my-ogx # Remove image -podman rmi my-llama-stack:custom +podman rmi my-ogx:custom ``` #### Connect LCORE to Manual Container Update `lightspeed-stack.yaml`: ```yaml -llama_stack: +ogx: use_as_library_client: false url: http://localhost:9000 # Use your custom port ``` diff --git a/docs/devel_doc/conversation_history.svg b/docs/devel_doc/conversation_history.svg index f339b3408..48c2d589a 100644 --- a/docs/devel_doc/conversation_history.svg +++ b/docs/devel_doc/conversation_history.svg @@ -1,7 +1,7 @@ - + @@ -191,11 +191,11 @@
-
llama-stack
+
ogx
- llama-stack + ogx
@@ -544,11 +544,11 @@
-
Llama Stack
Configuration
+
OGX
Configuration
- Llama Stack... + OGX Configuration diff --git a/docs/devel_doc/conversations_api.md b/docs/devel_doc/conversations_api.md index 2134df6b7..424f2f01d 100644 --- a/docs/devel_doc/conversations_api.md +++ b/docs/devel_doc/conversations_api.md @@ -166,9 +166,16 @@ Conversations are stored in **two databases**: - `openai_conversations`: Stores conversation metadata - `conversation_items`: Stores individual messages/turns in conversations -**Configuration (in OGX `run.yaml` / library client config):** +**Configuration (in OGX `run.yaml` / library client config; see `examples/run.yaml`):** ```yaml +apis: +- conversations +# ... storage: + backends: + sql_default: + type: sql_sqlite + db_path: ${env.SQL_STORE_PATH:=~/.llama/storage/sql_store.db} stores: conversations: table_name: openai_conversations @@ -509,6 +516,6 @@ Calling `/v3/conversations/{conversation_id}` returns empty `chat_history`. ## References - [OpenAI Responses API Documentation](https://platform.openai.com/docs/api-reference/responses) -- [OGX Documentation](https://github.com/meta-llama/llama-stack) +- [OGX Documentation](https://github.com/ogx-ai/ogx) - [LCS Configuration Guide](./config.md) - [LCS Getting Started Guide](./getting_started.md) diff --git a/docs/devel_doc/core2llama-stack_interface.png b/docs/devel_doc/core2ogx_interface.png similarity index 100% rename from docs/devel_doc/core2llama-stack_interface.png rename to docs/devel_doc/core2ogx_interface.png diff --git a/docs/devel_doc/openapi.md b/docs/devel_doc/openapi.md index c1cdba272..f1116d4db 100644 --- a/docs/devel_doc/openapi.md +++ b/docs/devel_doc/openapi.md @@ -6098,7 +6098,7 @@ Global service configuration. |-------|------|-------------| | name | string | Name of the service. That value will be used in REST API endpoints. | | service | | This section contains Lightspeed Core Stack service configuration. | -| llama_stack | | This section contains OGX configuration. Lightspeed Core Stack service can call OGX in library mode or in server mode. | +| ogx | | This section contains OGX configuration. Lightspeed Core Stack service can call OGX in library mode or in server mode. | | user_data_collection | | This section contains configuration for subsystem that collects user data(transcription history and feedbacks). | | database | | Configuration for database to store conversation IDs and other runtime data | | mcp_servers | array | MCP (Model Context Protocol) servers provide tools and capabilities to the AI agents. These are configured in this section. Only MCP servers defined in the lightspeed-stack.yaml configuration are available to the agents. Tools configured in the OGX run.yaml are not accessible to lightspeed-core agents. | diff --git a/docs/devel_doc/persistent_storage.svg b/docs/devel_doc/persistent_storage.svg index 5332f3ad6..753c86a57 100644 --- a/docs/devel_doc/persistent_storage.svg +++ b/docs/devel_doc/persistent_storage.svg @@ -1,7 +1,7 @@ - + @@ -43,11 +43,11 @@
-
llama-stack
+
ogx
- llama-stack + ogx
@@ -285,11 +285,11 @@
-
Llama Stack
Configuration
+
OGX
Configuration
- Llama Stack... + OGX Configuration @@ -336,11 +336,11 @@
-
llama-stack
+
ogx
- llama-stack + ogx @@ -578,11 +578,11 @@
-
Llama Stack
Configuration
+
OGX
Configuration
- Llama Stack... + OGX Configuration diff --git a/docs/devel_doc/providers.md b/docs/devel_doc/providers.md index 8fae59996..9df629460 100644 --- a/docs/devel_doc/providers.md +++ b/docs/devel_doc/providers.md @@ -106,14 +106,14 @@ inference: model_validation: false # added automatically by Lightspeed enrichment ``` -**How it works:** OGX defers Azure authentication to inference time. Lightspeed acquires Entra ID tokens at runtime and passes them via the `X-LlamaStack-Provider-Data` header (`azure_api_key`, `azure_api_base`). +**How it works:** OGX defers Azure authentication to inference time. Lightspeed acquires Entra ID tokens at runtime and passes them via the `X-OGX-Provider-Data` header (`azure_api_key`, `azure_api_base`). #### Access Token Lifecycle and Management **Lightspeed startup (library and service mode):** 1. Lightspeed reads your Entra ID configuration 2. Does not acquire or cache access tokens at startup—authentication is deferred until request time -3. Initializes the OGX client without Azure credentials; credentials are supplied later via `X-LlamaStack-Provider-Data` when an Azure model is used +3. Initializes the OGX client without Azure credentials; credentials are supplied later via `X-OGX-Provider-Data` when an Azure model is used **OGX service startup (container mode):** 1. Config enrichment sets `model_validation: false` on the Azure provider @@ -123,12 +123,12 @@ inference: **During inference requests:** 1. Before each request, Lightspeed checks if the token has expired 2. If expired, a new token is automatically acquired and cached in memory -3. The token is passed via `X-LlamaStack-Provider-Data` (library and service mode) +3. The token is passed via `X-OGX-Provider-Data` (library and service mode) **Token security:** - Access tokens are wrapped in `SecretStr` to prevent accidental logging - Tokens are cached in `AzureEntraIDManager` singleton class -- Inference uses `X-LlamaStack-Provider-Data` headers +- Inference uses `X-OGX-Provider-Data` headers - Each Uvicorn worker maintains its own token lifecycle independently **Token validity:** @@ -157,15 +157,15 @@ make run CONFIG=examples/lightspeed-stack-azure-entraid-lib.yaml ```bash # Terminal 1: Start OGX service with Azure Entra ID config -make run-llama-stack CONFIG=examples/lightspeed-stack-azure-entraid-service.yaml LLAMA_STACK_CONFIG=examples/azure-run.yaml +make run-ogx-local CONFIG=examples/lightspeed-stack-azure-entraid-service.yaml OGX_CONFIG=examples/azure-run.yaml # Terminal 2: Start Lightspeed (after OGX is ready) make run CONFIG=examples/lightspeed-stack-azure-entraid-service.yaml ``` -**Note:** The `make run-llama-stack` command accepts two variables: +**Note:** The `make run-ogx-local` command accepts two variables: - `CONFIG` - Lightspeed configuration file (default: `lightspeed-stack.yaml`) -- `LLAMA_STACK_CONFIG` - OGX configuration file to enrich and run (default: `run.yaml`) +- `OGX_CONFIG` - OGX configuration file to enrich and run (default: `run.yaml`) --- @@ -288,7 +288,7 @@ Shields are owned by LCORE (configured under `shields:` block), not as OGX `prov Run the following command to find out required dependencies for the desired provider (or check the tables above): ```bash - uv run llama stack list-providers + uv run ogx stack list-providers ``` Edit your `pyproject.toml` and add the required pip packages for the provider into `ogxlibdev` section: ```toml @@ -370,7 +370,7 @@ Shields are owned by LCORE (configured under `shields:` block), not as OGX `prov If you are running OGX as a standalone service, restart it with: ```bash - uv run llama stack run run.yaml + uv run ogx stack run run.yaml ``` If you are running it within Lightspeed Core, use: ```bash diff --git a/docs/devel_doc/query_endpoint.svg b/docs/devel_doc/query_endpoint.svg index a025f12d4..d96a4b102 100644 --- a/docs/devel_doc/query_endpoint.svg +++ b/docs/devel_doc/query_endpoint.svg @@ -23,10 +23,10 @@ Auth Auth - - Llama Stack Client - - Llama Stack Client + + OGX Client + + OGX Client Cache diff --git a/docs/devel_doc/streaming_query_endpoint.svg b/docs/devel_doc/streaming_query_endpoint.svg index ed35f7439..054c0ca4c 100644 --- a/docs/devel_doc/streaming_query_endpoint.svg +++ b/docs/devel_doc/streaming_query_endpoint.svg @@ -22,10 +22,10 @@ Auth Auth - - Llama Stack Client - - Llama Stack Client + + OGX Client + + OGX Client Stream build event @@ -65,7 +65,7 @@ loop - [For each chunk from LlamaStack] + [For each chunk from OGX] diff --git a/docs/user_doc/a2a_protocol.md b/docs/user_doc/a2a_protocol.md index 41125f314..728d26045 100644 --- a/docs/user_doc/a2a_protocol.md +++ b/docs/user_doc/a2a_protocol.md @@ -789,5 +789,5 @@ The protocol version is included in the agent card response and indicates which ## References - [A2A Protocol Specification](https://github.com/google/A2A) -- [OGX Documentation](https://llama-stack.readthedocs.io/) +- [OGX Documentation](https://ogx-ai.github.io/) - [FastAPI Documentation](https://fastapi.tiangolo.com/) diff --git a/docs/user_doc/both_services_in_container.svg b/docs/user_doc/both_services_in_container.svg index 8f20cce84..c01084d88 100644 --- a/docs/user_doc/both_services_in_container.svg +++ b/docs/user_doc/both_services_in_container.svg @@ -1,7 +1,7 @@ - + @@ -151,7 +151,7 @@
-
llama-stack as separate server
+
OGX as separate server
diff --git a/docs/user_doc/byok_guide.md b/docs/user_doc/byok_guide.md index 16de6160b..d18fe99c0 100644 --- a/docs/user_doc/byok_guide.md +++ b/docs/user_doc/byok_guide.md @@ -327,7 +327,7 @@ Both modes can be enabled simultaneously. Choose based on your latency and contr > [!TIP] > A ready-to-use example combining BYOK and OKP is available at -> [`examples/lightspeed-stack-byok-okp-rag.yaml`](../examples/lightspeed-stack-byok-okp-rag.yaml). +> [`examples/lightspeed-stack-byok-okp-rag.yaml`](../../examples/lightspeed-stack-byok-okp-rag.yaml). --- @@ -463,7 +463,7 @@ rag: > [!TIP] > A complete working example combining BYOK and OKP is available at -> [`examples/lightspeed-stack-byok-okp-rag.yaml`](../examples/lightspeed-stack-byok-okp-rag.yaml). +> [`examples/lightspeed-stack-byok-okp-rag.yaml`](../../examples/lightspeed-stack-byok-okp-rag.yaml). --- diff --git a/docs/user_doc/config.html b/docs/user_doc/config.html index f5baed350..56c71bbf1 100644 --- a/docs/user_doc/config.html +++ b/docs/user_doc/config.html @@ -2693,7 +2693,7 @@

UnifiedOgxConfig

Backend-specific knobs for unified-mode OGX synthesis.

Per Decision S5 of the design spike, backend-agnostic high-level sections (inference, …) live at the configuration root, not here. This -block holds only the Llama-Stack-specific synthesis controls: which +block holds only the OGX-specific synthesis controls: which baseline to start from, an optional profile file, and a raw native_override escape hatch.

Attributes: baseline: Synthesis starting point. “default” begins from diff --git a/docs/user_doc/config.json b/docs/user_doc/config.json index ebc9e180b..7e3a918c7 100644 --- a/docs/user_doc/config.json +++ b/docs/user_doc/config.json @@ -2249,7 +2249,7 @@ }, "UnifiedOgxConfig": { "additionalProperties": false, - "description": "Backend-specific knobs for unified-mode OGX synthesis.\n\nPer Decision S5 of the design spike, backend-agnostic high-level sections\n(inference, ...) live at the configuration root, not here. This block holds\nonly the Llama-Stack-specific synthesis controls: which baseline to start\nfrom, an optional profile file, and a raw native_override escape hatch.\n\nAttributes:\n baseline: Synthesis starting point. \"default\" begins from LCORE's\n built-in baseline (src/data/default_run.yaml); \"empty\" begins from\n an empty dict (used by the migration tool for an exact round-trip).\n Ignored when `profile` is set.\n profile: Optional path to a user-authored run.yaml-shaped file used as\n the synthesis baseline. Relative paths resolve against the directory\n of the loaded lightspeed-stack.yaml.\n native_override: Raw OGX schema deep-merged last (maps merge\n recursively, lists and scalars replace). The escape hatch for\n anything the high-level sections do not express.", + "description": "Backend-specific knobs for unified-mode OGX synthesis.\n\nPer Decision S5 of the design spike, backend-agnostic high-level sections\n(inference, ...) live at the configuration root, not here. This block holds\nonly the OGX-specific synthesis controls: which baseline to start\nfrom, an optional profile file, and a raw native_override escape hatch.\n\nAttributes:\n baseline: Synthesis starting point. \"default\" begins from LCORE's\n built-in baseline (src/data/default_run.yaml); \"empty\" begins from\n an empty dict (used by the migration tool for an exact round-trip).\n Ignored when `profile` is set.\n profile: Optional path to a user-authored run.yaml-shaped file used as\n the synthesis baseline. Relative paths resolve against the directory\n of the loaded lightspeed-stack.yaml.\n native_override: Raw OGX schema deep-merged last (maps merge\n recursively, lists and scalars replace). The escape hatch for\n anything the high-level sections do not express.", "properties": { "baseline": { "default": "default", diff --git a/docs/user_doc/config.md b/docs/user_doc/config.md index 237c6f28c..e53138fde 100644 --- a/docs/user_doc/config.md +++ b/docs/user_doc/config.md @@ -1043,7 +1043,7 @@ Backend-specific knobs for unified-mode OGX synthesis. Per Decision S5 of the design spike, backend-agnostic high-level sections (inference, ...) live at the configuration root, not here. This block holds -only the Llama-Stack-specific synthesis controls: which baseline to start +only the OGX-specific synthesis controls: which baseline to start from, an optional profile file, and a raw native_override escape hatch. During synthesis from the default baseline or a profile, LCORE ensures the diff --git a/docs/user_doc/config.puml b/docs/user_doc/config.puml index 22ff4d458..9cebc782d 100644 --- a/docs/user_doc/config.puml +++ b/docs/user_doc/config.puml @@ -179,10 +179,10 @@ class "JwtRoleRule" as src.models.config.JwtRoleRule { check_regex_pattern() -> Self check_roles() -> Self } -class "LlamaStackConfiguration" as src.models.config.LlamaStackConfiguration { +class "OgxConfiguration" as src.models.config.OgxConfiguration { allow_degraded_mode : Optional[bool] api_key : Optional[SecretStr] - config : Optional['UnifiedLlamaStackConfig'] + config : Optional['UnifiedOgxConfig'] library_client_config_path : Optional[str] max_retries retry_delay @@ -307,7 +307,7 @@ class "UnifiedInferenceProvider" as src.models.config.UnifiedInferenceProvider { id : str | None type : Literal['openai', 'ollama', 'vllm', 'sentence_transformers', 'azure', 'vertexai', 'watsonx', 'vllm_rhaiis', 'vllm_rhel_ai'] } -class "UnifiedLlamaStackConfig" as src.models.config.UnifiedLlamaStackConfig { +class "UnifiedOgxConfig" as src.models.config.UnifiedOgxConfig { baseline : Literal['default', 'empty'] native_override : dict[str, object] profile : Optional[str] @@ -339,7 +339,7 @@ src.models.config.InferenceConfiguration --|> src.models.config.ConfigurationBas src.models.config.JwkConfiguration --|> src.models.config.ConfigurationBase src.models.config.JwtConfiguration --|> src.models.config.ConfigurationBase src.models.config.JwtRoleRule --|> src.models.config.ConfigurationBase -src.models.config.LlamaStackConfiguration --|> src.models.config.ConfigurationBase +src.models.config.OgxConfiguration --|> src.models.config.ConfigurationBase src.models.config.ModelContextProtocolServer --|> src.models.config.ConfigurationBase src.models.config.OkpConfiguration --|> src.models.config.ConfigurationBase src.models.config.PostgreSQLDatabaseConfiguration --|> src.models.config.ConfigurationBase @@ -358,7 +358,7 @@ src.models.config.TLSConfiguration --|> src.models.config.ConfigurationBase src.models.config.TrustedProxyConfiguration --|> src.models.config.ConfigurationBase src.models.config.TrustedProxyServiceAccount --|> src.models.config.ConfigurationBase src.models.config.UnifiedInferenceProvider --|> src.models.config.ConfigurationBase -src.models.config.UnifiedLlamaStackConfig --|> src.models.config.ConfigurationBase +src.models.config.UnifiedOgxConfig --|> src.models.config.ConfigurationBase src.models.config.UserDataCollection --|> src.models.config.ConfigurationBase src.models.config.A2AStateConfiguration --* src.models.config.Configuration : a2a_state src.models.config.ApprovalsConfiguration --* src.models.config.Configuration : approvals @@ -371,7 +371,7 @@ src.models.config.DatabaseConfiguration --* src.models.config.Configuration : da src.models.config.InferenceConfiguration --* src.models.config.Configuration : inference src.models.config.JsonPathOperator --* src.models.config.JwtRoleRule : operator src.models.config.JwtConfiguration --* src.models.config.JwkConfiguration : jwt_configuration -src.models.config.LlamaStackConfiguration --* src.models.config.Configuration : ogx +src.models.config.OgxConfiguration --* src.models.config.Configuration : ogx src.models.config.OkpConfiguration --* src.models.config.Configuration : okp src.models.config.QuotaHandlersConfiguration --* src.models.config.Configuration : quota_handlers src.models.config.QuotaSchedulerConfiguration --* src.models.config.QuotaHandlersConfiguration : scheduler diff --git a/docs/user_doc/config.svg b/docs/user_doc/config.svg index 9b0884399..b1270e7d6 100644 --- a/docs/user_doc/config.svg +++ b/docs/user_doc/config.svg @@ -186,7 +186,7 @@ database deployment_environment : str inference - ogx + ogx mcp_servers : list[ModelContextProtocolServer] name : str okp @@ -348,16 +348,16 @@ check_regex_pattern() -> Self check_roles() -> Self - - - + + + - LlamaStackConfiguration + OgxConfiguration allow_degraded_mode : Optional[bool] api_key : Optional[SecretStr] - config : Optional['UnifiedLlamaStackConfig'] + config : Optional['UnifiedOgxConfig'] library_client_config_path : Optional[str] max_retries retry_delay @@ -608,12 +608,12 @@ type : Literal['openai', 'sentence_transformers', 'azure', 'vertexai', 'watsonx', 'vllm_rhaiis', 'vllm_rhel_ai'] - - - + + + - UnifiedLlamaStackConfig + UnifiedOgxConfig baseline : Literal['default', 'empty'] native_override : dict[str, object] @@ -734,9 +734,9 @@ - - - + + + @@ -829,9 +829,9 @@ - - - + + + @@ -905,11 +905,11 @@ jwt_configuration - - - + + + - ogx + ogx diff --git a/docs/user_doc/deployment_guide.md b/docs/user_doc/deployment_guide.md index 42aa10f01..3309ab876 100644 --- a/docs/user_doc/deployment_guide.md +++ b/docs/user_doc/deployment_guide.md @@ -90,12 +90,19 @@ ways it can drive the underlying OGX: optional [profile](#profiles) you author, the high-level `inference.providers` section, and a raw `native_override` escape hatch. All examples in this guide show unified mode first. -2. **Legacy two-file mode (deprecated).** `llama_stack.library_client_config_path` +2. **Legacy two-file mode (deprecated).** `ogx.library_client_config_path` + (the deprecated `llama_stack` YAML-section alias is still accepted) points at an external, hand-maintained `run.yaml`. This path is deprecated: since release 0.6 it logs a startup warning, and it is **removed in release 0.7**. See [Migrating from the legacy two-file configuration](#migrating-from-the-legacy-two-file-configuration). +> [!NOTE] +> The deprecated `llama_stack` YAML key remains accepted with a startup +> warning; use `ogx:` for new configuration. See +> [Migrating from the legacy two-file configuration](#migrating-from-the-legacy-two-file-configuration) +> and [v0.7.0 migration notes](../migrations/v0.7.0.md). + The two modes are mutually exclusive in one file — configuration loading fails if a unified synthesis input and `library_client_config_path` are both present. @@ -112,7 +119,7 @@ The OGX framework can be run as a standalone server and accessed via its the RES When this mode is selected, OGX is used as a regular Python library. This means that the library must be installed in the system Python environment, a user-level environment, or a virtual environment. All calls to OGX are performed via standard function or method calls: -![OGX as library](./llama_stack_as_library.svg) +![OGX as library](./ogx_as_library.svg) > [!NOTE] > Even when OGX is used as a library, it still requires a `run.yaml` @@ -150,14 +157,14 @@ for `baseline: empty` (use `native_override` there if you need MCP). Keep secrets out of the file: write `${env.MY_KEY}` environment references, which OGX resolves at startup. -**Referencing a profile.** Point `llama_stack.config.profile` at the file: +**Referencing a profile.** Point `ogx.config.profile` at the file: ```yaml name: Lightspeed Core Service (LCS) service: host: 0.0.0.0 port: 8080 -llama_stack: +ogx: use_as_library_client: true config: profile: ./profiles/openai-remote.yaml @@ -179,7 +186,7 @@ synthesizer evolves. When this mode is selected, OGX is started as a separate REST API service. All communication with OGX is performed via REST API calls, which means that OGX can run on a separate machine if needed. -![OGX as service](./llama_stack_as_service.svg) +![OGX as service](./ogx_as_service.svg) > [!NOTE] > The REST API schema and semantics can change at any time, especially before version 1.0.0 is released. By using *Lightspeed Core Service*, developers, users, and customers stay isolated from these incompatibilities. @@ -227,7 +234,7 @@ llama_stack: ```yaml # lightspeed-stack-unified.yaml name: LCS - llama_stack: + ogx: use_as_library_client: true config: baseline: empty @@ -289,17 +296,17 @@ The easiest option is to run OGX in a separate process. This means that there wi 1. Create a new directory outside of the lightspeed-stack project directory ```bash - mkdir /tmp/llama-stack-server + mkdir /tmp/ogx-server ``` -1. Copy the project file named `pyproject.llamastack.toml` into the new directory, renaming it to `pyproject.toml': +1. Copy the project file named `pyproject.ogx.toml` into the new directory, renaming it to `pyproject.toml`: ```bash - cp examples/pyproject.llamastack.toml /tmp/llama-stack-server/pyproject.toml + cp examples/pyproject.ogx.toml /tmp/ogx-server/pyproject.toml ``` 1. Run the following command to install all OGX dependencies in a new venv located in your new directory: ```bash - cd /tmp/llama-stack-server + cd /tmp/ogx-server uv sync ``` @@ -338,7 +345,7 @@ The easiest option is to run OGX in a separate process. This means that there wi 1. In the next step, we need to verify that it is possible to run a tool called `ogx`. It was installed into a Python virtual environment and therefore we have to run it via `uv run` command: ```bash - uv run llama + uv run ogx ``` 1. If the installation was successful, the following messages should be displayed on the terminal: ``` @@ -359,7 +366,7 @@ The easiest option is to run OGX in a separate process. This means that there wi ``` 1. If we try to run the OGX without configuring it, only the exception information is displayed (which is not very user-friendly): ```bash - uv run llama stack run + uv run ogx stack run ``` Output: ``` @@ -368,13 +375,13 @@ The easiest option is to run OGX in a separate process. This means that there wi File "/tmp/ramdisk/ogx-runner/.venv/bin/ogx", line 10, in sys.exit(main()) ^^^^^^ - File "/tmp/ramdisk/llama-stack-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/llama.py", line 53, in main + File "/tmp/ramdisk/ogx-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/llama.py", line 53, in main parser.run(args) - File "/tmp/ramdisk/llama-stack-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/llama.py", line 47, in run + File "/tmp/ramdisk/ogx-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/llama.py", line 47, in run args.func(args) - File "/tmp/ramdisk/llama-stack-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/stack/run.py", line 164, in _run_stack_run_cmd + File "/tmp/ramdisk/ogx-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/stack/run.py", line 164, in _run_stack_run_cmd server_main(server_args) - File "/tmp/ramdisk/llama-stack-runner/.venv/lib64/python3.12/site-packages/llama_stack/distribution/server/server.py", line 414, in main + File "/tmp/ramdisk/ogx-runner/.venv/lib64/python3.12/site-packages/llama_stack/distribution/server/server.py", line 414, in main elif args.template: ^^^^^^^^^^^^^ AttributeError: 'Namespace' object has no attribute 'template' @@ -387,7 +394,7 @@ The easiest option is to run OGX in a separate process. This means that there wi OGX needs to be configured properly. For using the default runnable OGX a file named `run.yaml` needs to be created. Copy the example `examples/run.yaml` from the lightspeed-stack project directory into your OGX directory. ```bash -cp examples/run.yaml /tmp/llama-stack-server +cp examples/run.yaml /tmp/ogx-server ``` @@ -399,7 +406,7 @@ cp examples/run.yaml /tmp/llama-stack-server ``` 1. Run the following command: ```bash - uv run llama stack run run.yaml + uv run ogx stack run run.yaml ``` 1. Check the output on terminal, it should look like: ``` @@ -648,7 +655,7 @@ cp examples/lightspeed-stack-lls-library.yaml lightspeed-stack.yaml The example is a unified-mode configuration: the `run.yaml` you created above is consumed as the synthesis [profile](#profiles) via -`llama_stack.config.profile` — there is no deprecated +`ogx.config.profile` — there is no deprecated `library_client_config_path` in it. @@ -831,12 +838,12 @@ a4982f43195537b9eb1cec510fe6655f245d6d4b7236a4759808115d5d719972 1. Create project file named `pyproject.toml` in this directory. This file should have the following content: ```toml [project] - name = "llama-stack-demo" + name = "ogx-demo" version = "0.1.0" description = "Default template for PDM package" authors = [] dependencies = [ - "llama-stack==0.2.22", + "ogx==1.2.5", "fastapi>=0.115.12", "opentelemetry-sdk>=1.34.0", "opentelemetry-exporter-otlp>=1.34.0", @@ -903,7 +910,7 @@ a4982f43195537b9eb1cec510fe6655f245d6d4b7236a4759808115d5d719972 1. In the next step, we need to verify that it is possible to run a tool called `ogx`. It was installed into a Python virtual environment and therefore we have to run it via `uv run` command: ```bash - uv run llama + uv run ogx ``` 1. If the installation was successful, the following messages should be displayed on the terminal: ```text @@ -924,7 +931,7 @@ a4982f43195537b9eb1cec510fe6655f245d6d4b7236a4759808115d5d719972 ``` 1. If we try to run the OGX without configuring it, only the exception information is displayed (which is not very user-friendly): ```bash - uv run llama stack run + uv run ogx stack run ``` Output: ``` @@ -933,13 +940,13 @@ a4982f43195537b9eb1cec510fe6655f245d6d4b7236a4759808115d5d719972 File "/tmp/ramdisk/ogx-runner/.venv/bin/ogx", line 10, in sys.exit(main()) ^^^^^^ - File "/tmp/ramdisk/llama-stack-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/llama.py", line 53, in main + File "/tmp/ramdisk/ogx-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/llama.py", line 53, in main parser.run(args) - File "/tmp/ramdisk/llama-stack-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/llama.py", line 47, in run + File "/tmp/ramdisk/ogx-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/llama.py", line 47, in run args.func(args) - File "/tmp/ramdisk/llama-stack-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/stack/run.py", line 164, in _run_stack_run_cmd + File "/tmp/ramdisk/ogx-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/stack/run.py", line 164, in _run_stack_run_cmd server_main(server_args) - File "/tmp/ramdisk/llama-stack-runner/.venv/lib64/python3.12/site-packages/llama_stack/distribution/server/server.py", line 414, in main + File "/tmp/ramdisk/ogx-runner/.venv/lib64/python3.12/site-packages/llama_stack/distribution/server/server.py", line 414, in main elif args.template: ^^^^^^^^^^^^^ AttributeError: 'Namespace' object has no attribute 'template' @@ -949,7 +956,7 @@ a4982f43195537b9eb1cec510fe6655f245d6d4b7236a4759808115d5d719972 #### OGX configuration -OGX needs to be configured properly. For using the default runnable OGX a file named `run.yaml` needs to be created. Use the example configuration from [examples/run.yaml](../examples/run.yaml). +OGX needs to be configured properly. For using the default runnable OGX a file named `run.yaml` needs to be created. Use the example configuration from [examples/run.yaml](../../examples/run.yaml). @@ -961,7 +968,7 @@ OGX needs to be configured properly. For using the default runnable OGX a file n ``` 1. Run the following command: ```bash - uv run llama stack run run.yaml + uv run ogx stack run run.yaml ``` 1. Check the output on terminal, it should look like: ```text @@ -1126,7 +1133,7 @@ service: workers: 1 color_log: true access_log: true -llama_stack: +ogx: use_as_library_client: false url: http://localhost:8321 api_key: xyzzy @@ -1173,7 +1180,7 @@ export OPENAI_API_KEY="sk-foo-bar-baz-my-key" #### OGX configuration -Create a file named `run.yaml`. Use the example configuration from [examples/run.yaml](../examples/run.yaml). +Create a file named `run.yaml`. Use the example configuration from [examples/run.yaml](../../examples/run.yaml). ### LCS configuration @@ -1190,7 +1197,7 @@ service: workers: 1 color_log: true access_log: true -llama_stack: +ogx: use_as_library_client: true config: profile: ./run.yaml diff --git a/docs/user_doc/lcs_in_container.svg b/docs/user_doc/lcs_in_container.svg index 6232cf444..c9fb42c5a 100644 --- a/docs/user_doc/lcs_in_container.svg +++ b/docs/user_doc/lcs_in_container.svg @@ -1,7 +1,7 @@ - + @@ -151,7 +151,7 @@

-
llama-stack as separate server
+
OGX as separate server
diff --git a/docs/user_doc/llama_stack_as_library.svg b/docs/user_doc/ogx_as_library.svg similarity index 99% rename from docs/user_doc/llama_stack_as_library.svg rename to docs/user_doc/ogx_as_library.svg index bfa8645b6..3d458f0f7 100644 --- a/docs/user_doc/llama_stack_as_library.svg +++ b/docs/user_doc/ogx_as_library.svg @@ -102,12 +102,12 @@
-
llama-stack as library (direct Python dependency)
+
OGX as library (direct Python dependency)
- llama-stack as libra... + OGX as library... diff --git a/docs/user_doc/llama_stack_as_service.svg b/docs/user_doc/ogx_as_service.svg similarity index 99% rename from docs/user_doc/llama_stack_as_service.svg rename to docs/user_doc/ogx_as_service.svg index 9062ed96a..f98538d1e 100644 --- a/docs/user_doc/llama_stack_as_service.svg +++ b/docs/user_doc/ogx_as_service.svg @@ -126,12 +126,12 @@
-
llama-stack as separate server
+
OGX as separate server
- llama-stack as separa... + OGX as separate server diff --git a/docs/user_doc/opentelemetry.md b/docs/user_doc/opentelemetry.md index 6cfac2c94..d3b90a265 100644 --- a/docs/user_doc/opentelemetry.md +++ b/docs/user_doc/opentelemetry.md @@ -84,7 +84,7 @@ The `otel` object is one section inside the full `configuration` payload returne "configuration": { "name": "lightspeed-stack", "service": { "..." }, - "llama_stack": { "..." }, + "ogx": { "..." }, "authentication": { "..." }, "authorization": { "..." }, "inference": { "..." }, diff --git a/docs/user_doc/rag_guide.md b/docs/user_doc/rag_guide.md index 0a763feb2..ee6c43cb5 100644 --- a/docs/user_doc/rag_guide.md +++ b/docs/user_doc/rag_guide.md @@ -99,7 +99,7 @@ Download a local embedding model such as `sentence-transformers/all-mpnet-base-v -BYOK knowledge sources are configured in the `rag.byok.stores` section of `lightspeed-stack.yaml`. The required configuration is automatically generated at startup when using `make run`, `make run-stack`, `docker-compose`, or library mode — no manual enrichment is needed. +BYOK knowledge sources are configured in the `rag.byok.stores` section of `lightspeed-stack.yaml`. The required configuration is automatically generated at startup when using `make run`, `make run-ogx`, `docker-compose`, or library mode — no manual enrichment is needed. ### FAISS example @@ -120,7 +120,7 @@ Where: - `db_path` is the path to the vector index (.db file in this case) - `vector_db_id` is the ID generated by rag-content during index creation (e.g. `vs_8c94967b-81cc-4028-a294-9cfac6fd9ae2`) -See the full working [config example](../examples/lightspeed-stack-byok-okp-rag.yaml) for more details. +See the full working [config example](../../examples/lightspeed-stack-byok-okp-rag.yaml) for more details. ### pgvector example @@ -178,8 +178,8 @@ Requirements: - When `providers` is empty, `default_provider` must be omitted - Provider `id` must match `[a-z0-9_-]+` and must not start with `byok_` - Applied in **unified** OGX synthesis only - (`llama_stack.use_as_library_client: true` with a synthesis input such as - `llama_stack.config`, `inference.providers`, or `vector_store.providers`) + (`ogx.use_as_library_client: true` with a synthesis input such as + `ogx.config`, `inference.providers`, or `vector_store.providers`) `default_provider` becomes `vector_stores.default_provider_id` and that provider's embedding model becomes `default_embedding_model` in the @@ -524,7 +524,7 @@ You are a helpful assistant with access to a 'knowledge_search' tool. When users --- # RAG annotations -The top-level `vector_stores` block in [`run.yaml`](../examples/run.yaml) may include `annotation_prompt_params` to control whether extra RAG annotation instructions are injected into the model prompt (for example, citation-style markers). The default configuration sets `enable_annotations: false` under that block to avoid unwanted annotations. +The top-level `vector_stores` block in [`run.yaml`](../../examples/run.yaml) may include `annotation_prompt_params` to control whether extra RAG annotation instructions are injected into the model prompt (for example, citation-style markers). The default configuration sets `enable_annotations: false` under that block to avoid unwanted annotations. When `vector_store` is configured, `default_provider` overwrites `vector_stores.default_provider_id` and `default_embedding_model` diff --git a/docs/user_doc/splunk.md b/docs/user_doc/splunk.md index 65de8fbbd..d78a77154 100644 --- a/docs/user_doc/splunk.md +++ b/docs/user_doc/splunk.md @@ -163,4 +163,4 @@ splunk: ## Extending to Other Endpoints -See [src/observability/README.md](../src/observability/README.md) for developer documentation on adding Splunk telemetry to additional endpoints. +See [src/observability/README.md](../../src/observability/README.md) for developer documentation on adding Splunk telemetry to additional endpoints. diff --git a/examples/lightspeed-stack-azure-entraid-service.yaml b/examples/lightspeed-stack-azure-entraid-service.yaml index f3f1a3186..eb77e4f48 100644 --- a/examples/lightspeed-stack-azure-entraid-service.yaml +++ b/examples/lightspeed-stack-azure-entraid-service.yaml @@ -12,7 +12,7 @@ ogx: use_as_library_client: false # Alternative for "as library use" # use_as_library_client: true - # library_client_config_path: + # library_client_config_path: url: http://localhost:8321 api_key: xyzzy user_data_collection: diff --git a/examples/profiles/inline-faiss.yaml b/examples/profiles/inline-faiss.yaml index 1e1004ac1..a88db319a 100644 --- a/examples/profiles/inline-faiss.yaml +++ b/examples/profiles/inline-faiss.yaml @@ -18,7 +18,7 @@ # # - type: vllm # # id: vllm-staging # # api_key_env: VLLM_STAGING_KEY -# llama_stack: +# ogx: # use_as_library_client: true # config: # profile: examples/profiles/inline-faiss.yaml diff --git a/examples/profiles/openai-remote.yaml b/examples/profiles/openai-remote.yaml index a212db622..268fc599d 100644 --- a/examples/profiles/openai-remote.yaml +++ b/examples/profiles/openai-remote.yaml @@ -4,7 +4,7 @@ # the synthesis baseline instead of LCORE's built-in default. Reference it from # lightspeed-stack.yaml: # -# llama_stack: +# ogx: # use_as_library_client: true # config: # profile: examples/profiles/openai-remote.yaml diff --git a/examples/pyproject.llamastack.toml b/examples/pyproject.ogx.toml similarity index 89% rename from examples/pyproject.llamastack.toml rename to examples/pyproject.ogx.toml index 9f873ee09..d75209b53 100644 --- a/examples/pyproject.llamastack.toml +++ b/examples/pyproject.ogx.toml @@ -1,10 +1,12 @@ [project] -name = "llama-stack-demo" +name = "ogx-demo" version = "0.1.0" description = "Default template for PDM package" authors = [] dependencies = [ - "llama-stack==0.2.22", + "ogx==1.2.5", + "ogx-api==1.2.5", + "ogx-client==1.2.5", "fastapi>=0.115.12", "opentelemetry-sdk>=1.34.0", "opentelemetry-exporter-otlp>=1.34.0", diff --git a/examples/quota-limiter-configuration-pg.yaml b/examples/quota-limiter-configuration-pg.yaml index dfe7e8502..9883ebaaf 100644 --- a/examples/quota-limiter-configuration-pg.yaml +++ b/examples/quota-limiter-configuration-pg.yaml @@ -8,11 +8,11 @@ service: access_log: true ogx: # Uses a remote OGX service - # The instance would have already been started with a llama-stack-run.yaml file + # The instance would have already been started with an OGX run.yaml file use_as_library_client: false # Alternative for "as library use" # use_as_library_client: true - # library_client_config_path: + # library_client_config_path: url: http://localhost:8321 api_key: xyzzy user_data_collection: diff --git a/examples/quota-limiter-configuration-sqlite.yaml b/examples/quota-limiter-configuration-sqlite.yaml index 757182436..a083ca626 100644 --- a/examples/quota-limiter-configuration-sqlite.yaml +++ b/examples/quota-limiter-configuration-sqlite.yaml @@ -8,11 +8,11 @@ service: access_log: true ogx: # Uses a remote OGX service - # The instance would have already been started with a llama-stack-run.yaml file + # The instance would have already been started with an OGX run.yaml file use_as_library_client: false # Alternative for "as library use" # use_as_library_client: true - # library_client_config_path: + # library_client_config_path: url: http://localhost:8321 api_key: xyzzy user_data_collection: diff --git a/scripts/ogx_tutorial.sh b/scripts/ogx_tutorial.sh index d3a3aaa6c..5847995b0 100755 --- a/scripts/ogx_tutorial.sh +++ b/scripts/ogx_tutorial.sh @@ -3,7 +3,7 @@ # OGX Tutorial - Interactive Guide # This tutorial demonstrates key features of the OGX server -LLAMA_STACK_URL="http://localhost:8321" +OGX_URL="http://localhost:8321" GREEN='\033[0;32m' BLUE='\033[0;34m' @@ -26,7 +26,7 @@ print_section() { print_header() { echo "" echo "🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀" - echo " WELCOME TO THE QUICK LLAMA STACK TUTORIAL " + echo " WELCOME TO THE QUICK OGX TUTORIAL " echo "🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀" echo "" } @@ -77,15 +77,15 @@ fi print_header if [ "$INTERACTIVE" = true ]; then - echo "This tutorial will guide you through the Llama Stack API step by step." + echo "This tutorial will guide you through the OGX API step by step." echo "You'll explore models, tools, shields, and see example API calls." wait_for_user fi # Section 0: What is OGX? -print_section "What is Llama Stack?" +print_section "What is OGX?" cat << 'EOF' -Llama Stack serves as the AI INTEGRATION LAYER - it's the middleware that abstracts +OGX serves as the AI INTEGRATION LAYER - it's the middleware that abstracts away the complexity of working with different LLM providers and provides a unified API for AI operations. @@ -113,13 +113,13 @@ KEY FEATURES: • Allows runtime switching between different LLM providers (OpenAI, Azure, etc.) DEPLOYMENT MODES: - 1. Service Mode: Llama Stack runs as a separate service - 2. Library Mode: Llama Stack embedded directly in the app + 1. Service Mode: OGX runs as a separate service + 2. Library Mode: OGX embedded directly in the app BOTTOM LINE: -Think of Llama Stack as a UNIVERSAL ADAPTER for AI operations. Instead of coding -directly against OpenAI's API, Azure's API, etc., Lightspeed Stack uses Llama -Stack's unified interface. This makes it easy to switch providers, add new +Think of OGX as a UNIVERSAL ADAPTER for AI operations. Instead of coding +directly against OpenAI's API, Azure's API, etc., Lightspeed Stack uses OGX's +unified interface. This makes it easy to switch providers, add new capabilities (like agents or RAG), and maintain consistent behavior across different LLM backends. @@ -128,26 +128,26 @@ wait_for_user # Section 1: Health Check print_section "1. Health Check" -echo "Let's verify the Llama Stack server is running..." -run_command "curl -s ${LLAMA_STACK_URL}/v1/health | ${JQ_CMD}" +echo "Let's verify the OGX server is running..." +run_command "curl -s ${OGX_URL}/v1/health | ${JQ_CMD}" wait_for_user # Section 2: Version print_section "2. Server Version" -echo "Checking Llama Stack version..." -run_command "curl -s ${LLAMA_STACK_URL}/v1/version | ${JQ_CMD}" +echo "Checking OGX version..." +run_command "curl -s ${OGX_URL}/v1/version | ${JQ_CMD}" wait_for_user # Section 3: Models print_section "3. Available Models" -echo "Llama Stack supports multiple models from different providers." +echo "OGX supports multiple models from different providers." echo "Let's see what models are available..." -run_command "curl -s ${LLAMA_STACK_URL}/v1/models | ${JQ_CMD}" +run_command "curl -s ${OGX_URL}/v1/models | ${JQ_CMD}" echo "" echo "Let me analyze the models for you..." -MODELS_JSON=$(curl -s ${LLAMA_STACK_URL}/v1/models) +MODELS_JSON=$(curl -s ${OGX_URL}/v1/models) if command -v jq &> /dev/null; then @@ -175,25 +175,25 @@ wait_for_user print_section "4. Safety Shields" echo "Shields provide content filtering and safety mechanisms." echo "Let's see what shields are configured..." -run_command "curl -s ${LLAMA_STACK_URL}/v1/shields | ${JQ_CMD}" +run_command "curl -s ${OGX_URL}/v1/shields | ${JQ_CMD}" wait_for_user # Section 5: Tool Groups print_section "5. Tool Groups" -echo "Llama Stack supports tool groups that organize related tools." +echo "OGX supports tool groups that organize related tools." echo "Let's explore available tool groups..." -run_command "curl -s ${LLAMA_STACK_URL}/v1/toolgroups | ${JQ_CMD}" +run_command "curl -s ${OGX_URL}/v1/toolgroups | ${JQ_CMD}" wait_for_user # Section 6: Tools print_section "6. Available Tools" echo "Tools allow agents to perform specific actions." echo "Let's see what tools are available..." -run_command "curl -s ${LLAMA_STACK_URL}/v1/tools | ${JQ_CMD}" +run_command "curl -s ${OGX_URL}/v1/tools | ${JQ_CMD}" echo "" echo "Let me show you the tool details..." -TOOLS_JSON=$(curl -s ${LLAMA_STACK_URL}/v1/tools) +TOOLS_JSON=$(curl -s ${OGX_URL}/v1/tools) if command -v jq &> /dev/null; then echo "" @@ -219,7 +219,7 @@ curl -X POST http://localhost:8321/v1/inference/chat-completion \ "messages": [ { "role": "user", - "content": "Explain Llama Stack in one sentence." + "content": "Explain OGX in one sentence." } ], "stream": false @@ -235,7 +235,7 @@ curl -X POST http://localhost:8321/v1/inference/embeddings \ -H 'Content-Type: application/json' \ -d '{ "model_id": "openai/text-embedding-3-small", - "contents": ["Llama Stack is awesome!"] + "contents": ["OGX is awesome!"] }' | jq . EOF @@ -268,12 +268,12 @@ EOF wait_for_user # Section 9: Integration -print_section "9. How Lightspeed Stack Uses Llama Stack" +print_section "9. How Lightspeed Stack Uses OGX" cat << 'EOF' -Lightspeed Stack integrates with Llama Stack to provide: +Lightspeed Stack integrates with OGX to provide: 1. 🤖 Multi-Provider LLM Support - - Llama Stack abstracts different providers (OpenAI, Azure, etc.) + - OGX abstracts different providers (OpenAI, Azure, etc.) - Lightspeed Stack uses this to support multiple models seamlessly 2. 🛡️ Safety & Content Filtering @@ -289,33 +289,33 @@ Lightspeed Stack integrates with Llama Stack to provide: - Simplifies AI integration in the Lightspeed Stack codebase Key Integration Points in Lightspeed Stack: -- src/client.py: Llama Stack client wrapper -- src/app/endpoints/: API endpoints using Llama Stack -- src/configuration.py: Configuration for Llama Stack connection +- src/client.py: OGX client wrapper +- src/app/endpoints/: API endpoints using OGX +- src/configuration.py: Configuration for OGX connection EOF wait_for_user # Section 10: Try It Now print_section "10. Try It Yourself!" echo "Let's make a real API call to see all available routes!" -run_command "curl -s ${LLAMA_STACK_URL}/v1/inspect/routes | ${JQ_CMD}" +run_command "curl -s ${OGX_URL}/v1/inspect/routes | ${JQ_CMD}" wait_for_user # Conclusion print_section "🎉 Tutorial Complete!" cat << 'EOF' You've learned about: -✅ Llama Stack server capabilities +✅ OGX server capabilities ✅ Available models (LLMs and embeddings) ✅ Safety shields for content filtering ✅ Tools and tool groups ✅ How to make API calls -✅ How Lightspeed Stack integrates with Llama Stack +✅ How Lightspeed Stack integrates with OGX Next Steps: 1. Explore the OpenAPI docs: http://localhost:8321/docs 2. Try the example commands above -3. Look at how Lightspeed Stack uses Llama Stack in src/client.py +3. Look at how Lightspeed Stack uses OGX in src/client.py 4. Experiment with different models and tools Resources: diff --git a/src/pydantic_ai_lightspeed/llamastack/README.md b/src/pydantic_ai_lightspeed/llamastack/README.md deleted file mode 100644 index 473d0e008..000000000 --- a/src/pydantic_ai_lightspeed/llamastack/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# List of source files stored in `src/pydantic_ai_lightspeed/llamastack` directory - diff --git a/tests/unit/pydantic_ai_lightspeed/llamastack/README.md b/tests/unit/pydantic_ai_lightspeed/llamastack/README.md deleted file mode 100644 index 7944b07e8..000000000 --- a/tests/unit/pydantic_ai_lightspeed/llamastack/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# List of source files stored in `tests/unit/pydantic_ai_lightspeed/llamastack` directory - From 747146e9de48ccaafa1478a8d6fb6e601ecda458 Mon Sep 17 00:00:00 2001 From: max-svistunov <88459374+max-svistunov@users.noreply.github.com> Date: Fri, 11 Sep 2026 15:24:53 +0200 Subject: [PATCH 053/120] LCORE-4069: compare JSON-in-string assertions by content, not by key order (#2647) The E2E Tests workflow has failed on every merge to main since 2026-09-03 (first red run: the merge of #2601). Both the library and server "skills" shards fail on the same two scenarios: tests/e2e/features/skills.feature:694 Skills directory path discovers all skills in subdirectories via query tests/e2e/features/skills.feature:724 ... via streaming_query The assertion compares the tool_results content field: expected: {"echo":"Echo back ...","summarize":"Summarize text ..."} actual: {"summarize":"Summarize text ...","echo":"Echo back ..."} Same two pairs; only the key order differs. The order is filesystem order, not a stable contract. pydantic_ai_skills discovers skills with root_dir.glob('**/SKILL.md'), which is not sorted, and its list_skills tool builds the result from that dict, so the serialized key order follows the scan. validate_json_partially() then compared the two serialized objects as raw strings, byte for byte. The assertion has always depended on filesystem order; it started failing once the runners produced the other order. validate_json_partially() now compares by parsed content when both the expected and the actual value are strings holding a JSON object or array. The comparison stays exact: - Same keys and same values. Relaxing it to the partial semantics the function uses elsewhere would silently weaken every existing assertion over an embedded JSON document, and the scenario is named "discovers all skills". - Same JSON types. Plain == accepts true for 1, false for 0 and 1 for 1.0, which the raw string comparison rejected, so values are compared together with their types. - Array element order still matters; only object key order is ignored. - Bare scalar strings are not parsed and keep the verbatim comparison. The new branch runs only when the two values already differ, so it cannot break an assertion that passes today. Reordering the expected literal in the feature file would not be a fix: it only moves the flake to the next filesystem layout. --- tests/e2e/utils/utils.py | 72 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tests/e2e/utils/utils.py b/tests/e2e/utils/utils.py index 2fc8efd27..fb35203a3 100644 --- a/tests/e2e/utils/utils.py +++ b/tests/e2e/utils/utils.py @@ -271,11 +271,73 @@ def wait_for_ogx_ready( return wait_for_container_health("ogx", max_attempts=max_attempts) +def _parsed_json_container(value: Any) -> Optional[Any]: + """Return ``value`` parsed as a JSON object or array, or None. + + Only objects and arrays qualify. A bare string such as ``"1e3"`` is plain + text here, not a document whose formatting may be normalised. + + Parameters: + ---------- + value: Candidate value, only strings are considered. + + Returns: + ------- + The parsed ``dict`` or ``list``, or None when ``value`` is not a string + holding a JSON object or array. + """ + if not isinstance(value, str): + return None + try: + parsed = json.loads(value) + except ValueError: + return None + return parsed if isinstance(parsed, (dict, list)) else None + + +def _json_values_equal(left: Any, right: Any) -> bool: + """Return True when two parsed JSON values are equal, including their types. + + Plain ``==`` is not enough: Python treats ``True == 1``, ``False == 0`` + and ``1 == 1.0`` as equal, so ``{"enabled": true}`` would match + ``{"enabled": 1}``. Object key order is ignored; array order is not. + + Parameters: + ---------- + left: First parsed JSON value. + right: Second parsed JSON value. + + Returns: + ------- + True when both values have the same JSON types and contents. + """ + if type(left) is not type(right): + return False + if isinstance(left, dict): + return left.keys() == right.keys() and all( + _json_values_equal(left[key], right[key]) for key in left + ) + if isinstance(left, list): + return len(left) == len(right) and all( + _json_values_equal(item_left, item_right) + for item_left, item_right in zip(left, right) + ) + return left == right + + def validate_json_partially(actual: Any, expected: Any) -> None: """Recursively validate that `actual` JSON contains all keys and values specified in `expected`. Extra elements/keys are ignored. Raises AssertionError if validation fails. + Values that are strings holding a serialized JSON object or array are + compared by parsed content rather than byte for byte, so a producer that + emits its keys in a different order still matches. The comparison stays + exact — same keys, same values, same JSON types, and array element order + still significant — because relaxing it to the partial semantics used + elsewhere would silently weaken every existing assertion over an embedded + JSON document. + Returns: None @@ -303,6 +365,16 @@ def validate_json_partially(actual: Any, expected: Any) -> None: ), f"No matching element found in list for schema item {schema_item}, got {actual}" else: + if actual != expected: + parsed_actual = _parsed_json_container(actual) + parsed_expected = _parsed_json_container(expected) + if parsed_actual is not None and parsed_expected is not None: + assert _json_values_equal(parsed_actual, parsed_expected), ( + f"JSON-in-string mismatch: expected {parsed_expected}, " + f"got {parsed_actual}" + ) + return + assert actual == expected, f"Value mismatch: expected {expected}, got {actual}" From ebd7849bae8bb42803e7351afff926b37554688f Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Tue, 4 Aug 2026 10:35:04 +0200 Subject: [PATCH 054/120] LCORE-2343: add unified-mode e2e configuration fixtures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create tests/e2e/configuration/unified-mode/ with library-mode/server-mode variants (same two-subdir layout configure_service resolves), covering the five unified-mode feature files: - unified-providers: minimal unified config driven only by top-level inference.providers over the default baseline (R1/S5); openai-specific. - unified-config-only / unified-relative-profile: profile: run.yaml — the CI-materialized repo-root run.yaml as baseline, provider-agnostic (R1/R8; two files because the features pin the intents separately). - unified-absolute-profile: container-absolute profile paths, differing per mode subdir (/app-root vs /opt/app-root mounts). - unified-native-override-{scalar,list}: R5 replacement semantics fixtures, synthesis-only, never booted. - invalid-{providers,config}-and-legacy: mutual-exclusion validation fixtures (R3); invalid-version-legacy-unified-body: R11 marker mismatch (needs LCORE-2872's cross-validation to fail for the right reason). - legacy-for-migration: legacy half of the migration fixture pair, kept free of enrichment sections so migrate-then-synthesize round-trips losslessly (LCORE-3370). Every fixture is validated against the real Configuration model: bootable ones load, invalid ones fail with the intended error. The test-generated lightspeed-stack-unified-migrated.yaml is gitignored, and the directory README documents each fixture's purpose. --- .gitignore | 3 ++ .../e2e/configuration/unified-mode/README.md | 24 ++++++++++++++ ...speed-stack-invalid-config-and-legacy.yaml | 25 ++++++++++++++ ...ed-stack-invalid-providers-and-legacy.yaml | 31 +++++++++++++++++ ...k-invalid-version-legacy-unified-body.yaml | 31 +++++++++++++++++ ...lightspeed-stack-legacy-for-migration.yaml | 23 +++++++++++++ ...tspeed-stack-unified-absolute-profile.yaml | 24 ++++++++++++++ .../lightspeed-stack-unified-config-only.yaml | 24 ++++++++++++++ ...ed-stack-unified-native-override-list.yaml | 29 ++++++++++++++++ ...-stack-unified-native-override-scalar.yaml | 28 ++++++++++++++++ .../lightspeed-stack-unified-providers.yaml | 29 ++++++++++++++++ ...tspeed-stack-unified-relative-profile.yaml | 24 ++++++++++++++ ...speed-stack-invalid-config-and-legacy.yaml | 27 +++++++++++++++ ...ed-stack-invalid-providers-and-legacy.yaml | 33 +++++++++++++++++++ ...k-invalid-version-legacy-unified-body.yaml | 33 +++++++++++++++++++ ...lightspeed-stack-legacy-for-migration.yaml | 23 +++++++++++++ ...tspeed-stack-unified-absolute-profile.yaml | 26 +++++++++++++++ .../lightspeed-stack-unified-config-only.yaml | 26 +++++++++++++++ ...ed-stack-unified-native-override-list.yaml | 31 +++++++++++++++++ ...-stack-unified-native-override-scalar.yaml | 30 +++++++++++++++++ .../lightspeed-stack-unified-providers.yaml | 31 +++++++++++++++++ ...tspeed-stack-unified-relative-profile.yaml | 26 +++++++++++++++ 22 files changed, 581 insertions(+) create mode 100644 tests/e2e/configuration/unified-mode/README.md create mode 100644 tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-config-and-legacy.yaml create mode 100644 tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-providers-and-legacy.yaml create mode 100644 tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml create mode 100644 tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-legacy-for-migration.yaml create mode 100644 tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-absolute-profile.yaml create mode 100644 tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-config-only.yaml create mode 100644 tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-native-override-list.yaml create mode 100644 tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-native-override-scalar.yaml create mode 100644 tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-providers.yaml create mode 100644 tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-relative-profile.yaml create mode 100644 tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-config-and-legacy.yaml create mode 100644 tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-providers-and-legacy.yaml create mode 100644 tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml create mode 100644 tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-legacy-for-migration.yaml create mode 100644 tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-absolute-profile.yaml create mode 100644 tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-config-only.yaml create mode 100644 tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-native-override-list.yaml create mode 100644 tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-native-override-scalar.yaml create mode 100644 tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-providers.yaml create mode 100644 tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-relative-profile.yaml diff --git a/.gitignore b/.gitignore index 3d7a1f08f..3225ab2a9 100644 --- a/.gitignore +++ b/.gitignore @@ -202,3 +202,6 @@ local-run.yaml .sisyphus/ # Per-developer feature design overrides (see docs/contributing/feature-design.config) .feature-design.config.local + +# Generated at e2e test time by the unified-mode --migrate-config step +tests/e2e/configuration/unified-mode/**/lightspeed-stack-unified-migrated.yaml diff --git a/tests/e2e/configuration/unified-mode/README.md b/tests/e2e/configuration/unified-mode/README.md new file mode 100644 index 000000000..79eaaabe0 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/README.md @@ -0,0 +1,24 @@ +# Unified-mode e2e configuration fixtures + +Fixtures for the five `unified-mode-*.feature` files (LCORE-2341/LCORE-2343). +Same layout as the parent directory: `library-mode/` and `server-mode/` +variants differing only in the `llama_stack` block; the harness resolves +`//` via the standard `configure_service` logic. + +All profile-based fixtures reference `run.yaml` — the repo-root copy the CI +harness materializes from `tests/e2e/configs/run-.yaml` — so they stay +provider-agnostic across the providers matrix. + +| Fixture | Purpose | +|---|---| +| `lightspeed-stack-unified-providers.yaml` | Minimal unified config driven only by top-level `inference.providers` (default baseline, R1/S5). openai-specific — used by `@openai-only` scenarios. | +| `lightspeed-stack-unified-config-only.yaml` | Unified config driven only by `llama_stack.config` (`profile: run.yaml`, R1). | +| `lightspeed-stack-unified-relative-profile.yaml` | Same shape as config-only; exists to pin R8 (relative `profile:` resolves against the config file's directory) as a distinct intent. | +| `lightspeed-stack-unified-absolute-profile.yaml` | `profile:` as a container-absolute path (differs per mode subdir). | +| `lightspeed-stack-unified-native-override-scalar.yaml` | `native_override` replaces an overlapping scalar key (R5). Synthesis-only; never booted. | +| `lightspeed-stack-unified-native-override-list.yaml` | `native_override` replaces an overlapping list wholesale (R5). Synthesis-only; never booted. | +| `lightspeed-stack-invalid-providers-and-legacy.yaml` | INVALID: `inference.providers` + `library_client_config_path` (mutual exclusion, R3). Validation-only. | +| `lightspeed-stack-invalid-config-and-legacy.yaml` | INVALID: `llama_stack.config` + `library_client_config_path` (R3). Validation-only. | +| `lightspeed-stack-invalid-version-legacy-unified-body.yaml` | INVALID: `config_format_version: legacy` on a unified-shaped body (R11, LCORE-2872). Validation-only. | +| `lightspeed-stack-legacy-for-migration.yaml` | Legacy half of "the legacy migration fixture pair"; paired with the repo-root `run.yaml`. Deliberately free of enrichment sections so migrate→synthesize round-trips losslessly (see LCORE-3370). | +| `lightspeed-stack-unified-migrated.yaml` | Generated at test time by the `--migrate-config` step; gitignored and cleaned up after each scenario. | diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-config-and-legacy.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-config-and-legacy.yaml new file mode 100644 index 000000000..bdb8af911 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-config-and-legacy.yaml @@ -0,0 +1,25 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + config: + profile: run.yaml + # INVALID: config block plus the legacy path (mutual exclusion, R3) + library_client_config_path: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-providers-and-legacy.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-providers-and-legacy.yaml new file mode 100644 index 000000000..124fd453c --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-providers-and-legacy.yaml @@ -0,0 +1,31 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + # INVALID: synthesis input plus the legacy path (mutual exclusion, R3) + library_client_config_path: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini + # Unified synthesis input (Decision S5): the high-level provider entry + # replaces the default baseline's openai provider by id at synthesis time. + providers: + - type: openai + id: openai + api_key_env: OPENAI_API_KEY + allowed_models: + - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml new file mode 100644 index 000000000..9ae7b389d --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml @@ -0,0 +1,31 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini + # Unified synthesis input (Decision S5): the high-level provider entry + # replaces the default baseline's openai provider by id at synthesis time. + providers: + - type: openai + id: openai + api_key_env: OPENAI_API_KEY + allowed_models: + - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} +# INVALID: explicit legacy marker on a unified-shaped body (R11, LCORE-2872) +config_format_version: legacy diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-legacy-for-migration.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-legacy-for-migration.yaml new file mode 100644 index 000000000..6393142b5 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-legacy-for-migration.yaml @@ -0,0 +1,23 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + # Legacy two-file shape: external run.yaml, no synthesis input + library_client_config_path: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-absolute-profile.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-absolute-profile.yaml new file mode 100644 index 000000000..089fb0afb --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-absolute-profile.yaml @@ -0,0 +1,24 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + config: + # Absolute path as mounted in the library-mode container + profile: /app-root/run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-config-only.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-config-only.yaml new file mode 100644 index 000000000..b3df26828 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-config-only.yaml @@ -0,0 +1,24 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + config: + # Synthesis baseline: the CI-materialized run.yaml (provider-agnostic) + profile: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-native-override-list.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-native-override-list.yaml new file mode 100644 index 000000000..403cc7a00 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-native-override-list.yaml @@ -0,0 +1,29 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + config: + profile: run.yaml + # R5: lists replace wholesale - the synthesized apis must equal exactly + # this list, not a merge with the baseline's (never booted - synthesis only) + native_override: + apis: + - inference + - tool_runtime +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-native-override-scalar.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-native-override-scalar.yaml new file mode 100644 index 000000000..02f67f028 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-native-override-scalar.yaml @@ -0,0 +1,28 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + config: + profile: run.yaml + # R5: the raw escape hatch wins; this scalar replaces the baseline's + # safety.excluded_categories value wholesale (never booted - synthesis only) + native_override: + safety: + excluded_categories: unified-override-marker +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-providers.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-providers.yaml new file mode 100644 index 000000000..731c39b5d --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-providers.yaml @@ -0,0 +1,29 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini + # Unified synthesis input (Decision S5): the high-level provider entry + # replaces the default baseline's openai provider by id at synthesis time. + providers: + - type: openai + id: openai + api_key_env: OPENAI_API_KEY + allowed_models: + - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-relative-profile.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-relative-profile.yaml new file mode 100644 index 000000000..228b2d40b --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-relative-profile.yaml @@ -0,0 +1,24 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + config: + # R8: relative profile resolves against this file's loaded location + profile: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-config-and-legacy.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-config-and-legacy.yaml new file mode 100644 index 000000000..34dbfb06f --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-config-and-legacy.yaml @@ -0,0 +1,27 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate llama-stack service + use_as_library_client: false + url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + api_key: xyzzy + config: + profile: run.yaml + # INVALID: config block plus the legacy path (mutual exclusion, R3) + library_client_config_path: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-providers-and-legacy.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-providers-and-legacy.yaml new file mode 100644 index 000000000..6cbd5f50a --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-providers-and-legacy.yaml @@ -0,0 +1,33 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate llama-stack service + use_as_library_client: false + url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + api_key: xyzzy + # INVALID: synthesis input plus the legacy path (mutual exclusion, R3) + library_client_config_path: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini + # Unified synthesis input (Decision S5): the high-level provider entry + # replaces the default baseline's openai provider by id at synthesis time. + providers: + - type: openai + id: openai + api_key_env: OPENAI_API_KEY + allowed_models: + - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml new file mode 100644 index 000000000..f3f9b9cdc --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml @@ -0,0 +1,33 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate llama-stack service + use_as_library_client: false + url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + api_key: xyzzy +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini + # Unified synthesis input (Decision S5): the high-level provider entry + # replaces the default baseline's openai provider by id at synthesis time. + providers: + - type: openai + id: openai + api_key_env: OPENAI_API_KEY + allowed_models: + - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} +# INVALID: explicit legacy marker on a unified-shaped body (R11, LCORE-2872) +config_format_version: legacy diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-legacy-for-migration.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-legacy-for-migration.yaml new file mode 100644 index 000000000..76b2ac36d --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-legacy-for-migration.yaml @@ -0,0 +1,23 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate llama-stack service + use_as_library_client: false + url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + api_key: xyzzy +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-absolute-profile.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-absolute-profile.yaml new file mode 100644 index 000000000..1a1c4d8a1 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-absolute-profile.yaml @@ -0,0 +1,26 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate llama-stack service + use_as_library_client: false + url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + api_key: xyzzy + config: + # Absolute path as mounted in the llama-stack container + profile: /opt/app-root/run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-config-only.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-config-only.yaml new file mode 100644 index 000000000..3881fdf4e --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-config-only.yaml @@ -0,0 +1,26 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate llama-stack service + use_as_library_client: false + url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + api_key: xyzzy + config: + # Synthesis baseline: the CI-materialized run.yaml (provider-agnostic) + profile: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-native-override-list.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-native-override-list.yaml new file mode 100644 index 000000000..7454bab43 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-native-override-list.yaml @@ -0,0 +1,31 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate llama-stack service + use_as_library_client: false + url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + api_key: xyzzy + config: + profile: run.yaml + # R5: lists replace wholesale - the synthesized apis must equal exactly + # this list, not a merge with the baseline's (never booted - synthesis only) + native_override: + apis: + - inference + - tool_runtime +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-native-override-scalar.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-native-override-scalar.yaml new file mode 100644 index 000000000..3451fb526 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-native-override-scalar.yaml @@ -0,0 +1,30 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate llama-stack service + use_as_library_client: false + url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + api_key: xyzzy + config: + profile: run.yaml + # R5: the raw escape hatch wins; this scalar replaces the baseline's + # safety.excluded_categories value wholesale (never booted - synthesis only) + native_override: + safety: + excluded_categories: unified-override-marker +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-providers.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-providers.yaml new file mode 100644 index 000000000..4ca585947 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-providers.yaml @@ -0,0 +1,31 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate llama-stack service + use_as_library_client: false + url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + api_key: xyzzy +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini + # Unified synthesis input (Decision S5): the high-level provider entry + # replaces the default baseline's openai provider by id at synthesis time. + providers: + - type: openai + id: openai + api_key_env: OPENAI_API_KEY + allowed_models: + - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-relative-profile.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-relative-profile.yaml new file mode 100644 index 000000000..3c6c8512a --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-relative-profile.yaml @@ -0,0 +1,26 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate llama-stack service + use_as_library_client: false + url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + api_key: xyzzy + config: + # R8: relative profile resolves against this file's loaded location + profile: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini From cf637b81976f077629a01bad971a38f1bf710729 Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Tue, 4 Aug 2026 10:35:04 +0200 Subject: [PATCH 055/120] LCORE-2343: restore legacy library-mode boot coverage with a dedicated fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LCORE-2342 migrated the standard library-mode baseline to unified mode (config.profile: run.yaml), which silently changed what unified-mode-legacy.feature's library scenario exercises: it now boots the unified baseline, not the deprecated two-file path, so R2's library-mode legacy coverage was gone. Add lightspeed-stack-legacy.yaml — identical to the baseline except its llama_stack block uses the true legacy shape (use_as_library_client + library_client_config_path: run.yaml, no synthesis input) — and point the library scenario's Given at it. This is the one deliberate Gherkin edit in LCORE-2343, agreed with Maxim in planning; the server-mode scenario is untouched since container-side enrichment there is genuinely legacy. --- .../library-mode/lightspeed-stack-legacy.yaml | 46 +++++++++++++++++++ .../e2e/features/unified-mode-legacy.feature | 5 +- 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/configuration/library-mode/lightspeed-stack-legacy.yaml diff --git a/tests/e2e/configuration/library-mode/lightspeed-stack-legacy.yaml b/tests/e2e/configuration/library-mode/lightspeed-stack-legacy.yaml new file mode 100644 index 000000000..993f0d812 --- /dev/null +++ b/tests/e2e/configuration/library-mode/lightspeed-stack-legacy.yaml @@ -0,0 +1,46 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Legacy two-file shape (R2 deprecation window): external run.yaml consumed + # via library_client_config_path; no unified synthesis input. Kept as a + # dedicated fixture because the standard library-mode baseline migrated to + # unified mode in LCORE-2342, which silently removed legacy boot coverage. + use_as_library_client: true + library_client_config_path: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini +byok_rag: + - rag_id: e2e-test-docs + rag_type: inline::faiss + embedding_model: sentence-transformers/all-mpnet-base-v2 + embedding_dimension: 768 + vector_db_id: ${env.FAISS_VECTOR_STORE_ID} + db_path: ${env.KV_RAG_PATH:=~/.llama/storage/rag/kv_store.db} + score_multiplier: 1.0 + +rag: + tool: + - e2e-test-docs + +shields: + - name: pii-redaction + provider_id: redaction + config: + rules: + - pattern: '\d+' + replacement: '[NUM]' + diff --git a/tests/e2e/features/unified-mode-legacy.feature b/tests/e2e/features/unified-mode-legacy.feature index dea933804..371e1ccb0 100644 --- a/tests/e2e/features/unified-mode-legacy.feature +++ b/tests/e2e/features/unified-mode-legacy.feature @@ -12,7 +12,10 @@ Feature: Legacy two-file configuration during deprecation window @skip-in-server-mode Scenario: Legacy two-file configuration still boots and serves requests in library mode - Given The service uses the lightspeed-stack.yaml configuration + # lightspeed-stack-legacy.yaml (not the standard baseline): LCORE-2342 + # migrated the library-mode baseline to unified mode, so only a dedicated + # legacy-shaped fixture still exercises the deprecated two-file path (R2). + Given The service uses the lightspeed-stack-legacy.yaml configuration And The service is restarted When I access endpoint "readiness" using HTTP GET method Then The status code of the response is 200 From 2ae2f712438b0f7f35b280879ec501cb975cdf06 Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Tue, 4 Aug 2026 11:13:02 +0200 Subject: [PATCH 056/120] LCORE-2343: implement unified-mode step definitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add tests/e2e/features/steps/unified_mode.py — the 16 step patterns the validation, migration, and synthesis features need (boot and legacy resolve entirely through existing generic steps). Per the planning decisions: - All artifact steps operate on the ON-DISK configuration (the repo-root lightspeed-stack.yaml copy configure_service applied), never the live service. - Validation runs the service CLI (--dump-configuration) as a black-box subprocess from the repo root and asserts a non-zero exit, so the error-contains assertions can never pass against a healthy load. - Migration runs the real --migrate-config CLI; the output lands in the active mode subdir under the gitignored name later Gherkin references, and is cleaned up per scenario. - Synthesis runs the config CLI exactly as the server entrypoint does (unified auto-detection -> synthesize_to_file, giving the 0600 mode the permissions scenario asserts). Round-trip and override assertions parse YAML and compare data, never bytes; override assertions are self-referential against the fixture's native_override and additionally assert the baseline differed, so replacements can't pass vacuously. - The --synthesized-config-output scenario launches a short-lived local service from the library-mode fixture variant on a rewritten port and polls for the custom output file (the flag is library-mode-only by design; running containers cannot be restarted with new CLI args). - The startup-log step is mode-aware: in server mode the synthesis evidence is emitted by the llama-stack container (entrypoint + CLI), not the lightspeed-stack container the Gherkin names — asserted against the synthesizing container with the rationale documented in the step. behave --dry-run over the five features: 24 scenarios, 200 steps, zero undefined. --- tests/e2e/features/steps/unified_mode.py | 477 +++++++++++++++++++++++ 1 file changed, 477 insertions(+) create mode 100644 tests/e2e/features/steps/unified_mode.py diff --git a/tests/e2e/features/steps/unified_mode.py b/tests/e2e/features/steps/unified_mode.py new file mode 100644 index 000000000..880db4171 --- /dev/null +++ b/tests/e2e/features/steps/unified_mode.py @@ -0,0 +1,477 @@ +"""Step definitions for the unified-mode e2e features (LCORE-2343). + +Covers configuration validation, legacy-to-unified migration, and run.yaml +synthesis for the five ``unified-mode-*.feature`` files. + +Design rules (from the LCORE-2343 planning notes): + +- Validation, migration, and synthesis steps operate on the **on-disk** + configuration artifacts — never the live service. "The active + configuration" is the repo-root ``lightspeed-stack.yaml`` copy that + ``configure_service`` applied. +- Migration and synthesis run the real CLIs as subprocesses — exactly the + surface the server entrypoint and operators use — and assertions parse + the produced YAML (data equality, never byte comparison). +- The synthesis-log step is mode-aware: in server mode the synthesis + evidence is emitted by the llama-stack container (entrypoint + CLI), not + the lightspeed-stack container the Gherkin names; the scenario's intent + (R10: the synthesized path is logged at startup) is asserted against the + container that actually synthesizes. +""" + +import difflib +import os +import re +import shutil +import stat +import subprocess +import sys +import tempfile +import time +from pathlib import Path +from typing import Any, Optional + +import yaml +from behave import given, step, then, when # pyright: ignore +from behave.runner import Context + +# Generated by the --migrate-config step; matches the .gitignore entry. +MIGRATED_CONFIG_BASENAME = "lightspeed-stack-unified-migrated.yaml" +# Legacy half of "the legacy migration fixture pair"; its run.yaml half is +# the repo-root run.yaml the CI harness materializes. +MIGRATION_PAIR_LCS_BASENAME = "lightspeed-stack-legacy-for-migration.yaml" + +CLI_TIMEOUT_SECONDS = 120 +CUSTOM_OUTPUT_POLL_SECONDS = 60 + + +def _mode_subdir(context: Context) -> str: + """Return the mode fixture subdirectory name for the current harness mode.""" + return "library-mode" if context.is_library_mode else "server-mode" + + +def _config_dir(context: Context) -> Path: + """Resolve the active fixture directory, mode subdir included when present. + + Mirrors ``configure_service``'s resolution so files referenced by name in + Gherkin (fixture pairs, migrated output) land where that step finds them. + """ + base = Path( + getattr(context, "lightspeed_stack_config_directory", "") + or "tests/e2e/configuration" + ) + mode_base = base / _mode_subdir(context) + return mode_base if mode_base.is_dir() else base + + +def _active_config_path() -> Path: + """Return the on-disk active configuration (the applied repo-root copy).""" + return Path("lightspeed-stack.yaml") + + +def _run_cli( + args: list[str], cwd: Optional[Path] = None +) -> subprocess.CompletedProcess: + """Run a repo CLI as a subprocess, capturing output, never raising.""" + return subprocess.run( + [sys.executable, *args], + cwd=str(cwd) if cwd else None, + capture_output=True, + text=True, + timeout=CLI_TIMEOUT_SECONDS, + check=False, + ) + + +def _load_yaml(path: Path) -> Any: + """Parse a YAML file.""" + with open(path, "r", encoding="utf-8") as f: + return yaml.safe_load(f) + + +def _native_override(config_path: Path) -> dict[str, Any]: + """Extract llama_stack.config.native_override from a config file.""" + config = _load_yaml(config_path) + override = ((config.get("llama_stack") or {}).get("config") or {}).get( + "native_override" + ) + assert override, f"{config_path} carries no llama_stack.config.native_override" + return override + + +def _synthesized(context: Context) -> Path: + """Return the synthesized run.yaml path recorded by an earlier step.""" + path = getattr(context, "synthesized_run_yaml_path", None) + assert path, "no synthesis step ran before this assertion" + return Path(path) + + +# --------------------------------------------------------------------------- +# Validation (unified-mode-validation.feature) +# --------------------------------------------------------------------------- + + +@when("configuration validation is attempted for the active configuration") +def attempt_configuration_validation(context: Context) -> None: + """Validate the on-disk active configuration via the service CLI. + + Runs ``lightspeed_stack.py --dump-configuration -c lightspeed-stack.yaml`` + as a black-box subprocess: ``main()`` loads (and thereby validates) the + configuration before any dump handling, so a Pydantic validation failure + surfaces on stderr with a non-zero exit code. cwd is the repo root so the + invalid fixtures' ``library_client_config_path: run.yaml`` resolves to the + harness-materialized run.yaml and the captured failure is the intended + cross-field error, not a file-not-found. + """ + result = _run_cli( + [ + "src/lightspeed_stack.py", + "--dump-configuration", + "-c", + str(_active_config_path()), + ] + ) + context.validation_returncode = result.returncode + context.validation_output = result.stdout + result.stderr + assert result.returncode != 0, ( + "expected the active configuration to fail validation, but the load " + f"succeeded (rc=0). Output:\n{context.validation_output}" + ) + + +@then("the validation error contains {text}") +def validation_error_contains(context: Context, text: str) -> None: + """Assert the captured validation failure mentions the given text.""" + output = getattr(context, "validation_output", None) + assert output is not None, "no validation attempt ran before this assertion" + assert ( + text.strip() in output + ), f"validation error does not contain {text!r}. Full output:\n{output}" + + +# --------------------------------------------------------------------------- +# Migration (unified-mode-migration.feature) +# --------------------------------------------------------------------------- + + +@step("lightspeed-stack --migrate-config is run for the legacy migration fixture pair") +def run_migrate_config(context: Context) -> None: + """Migrate the legacy fixture pair into the active fixture directory. + + The pair is ``lightspeed-stack-legacy-for-migration.yaml`` (mode subdir) + plus the repo-root ``run.yaml`` the harness materializes. The output lands + in the same mode subdir under the name later Gherkin steps reference, so + ``configure_service`` can boot it; it is gitignored and cleaned up after + the scenario. + """ + pair_lcs = _config_dir(context) / MIGRATION_PAIR_LCS_BASENAME + pair_run = Path("run.yaml") + output = _config_dir(context) / MIGRATED_CONFIG_BASENAME + assert pair_lcs.is_file(), f"missing migration fixture {pair_lcs}" + assert pair_run.is_file(), "repo-root run.yaml (harness-materialized) missing" + + result = _run_cli( + [ + "src/lightspeed_stack.py", + "--migrate-config", + "--run-yaml", + str(pair_run), + "-c", + str(pair_lcs), + "--migrate-output", + str(output), + ] + ) + assert result.returncode == 0 and output.is_file(), ( + f"--migrate-config failed (rc={result.returncode}).\n" + f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" + ) + context.migrated_config_path = output + context.migration_pair_run_yaml = pair_run + context.add_cleanup(lambda: output.unlink(missing_ok=True)) + + +@then("the file {filename} contains {text}") +def file_contains(context: Context, filename: str, text: str) -> None: + """Assert a file in the active fixture directory contains a substring.""" + path = _config_dir(context) / filename.strip() + content = path.read_text(encoding="utf-8") + assert text.strip() in content, f"{path} does not contain {text!r}" + + +@then("the file {filename} does not contain {text}") +def file_does_not_contain(context: Context, filename: str, text: str) -> None: + """Assert a file in the active fixture directory lacks a substring.""" + path = _config_dir(context) / filename.strip() + content = path.read_text(encoding="utf-8") + assert text.strip() not in content, f"{path} unexpectedly contains {text!r}" + + +# --------------------------------------------------------------------------- +# Synthesis (unified-mode-synthesis.feature + migration round-trip) +# --------------------------------------------------------------------------- + + +@step("the active unified configuration is synthesized to run.yaml") +def synthesize_active_configuration(context: Context) -> None: + """Synthesize a run.yaml from the on-disk unified configuration. + + Runs the config CLI exactly as the server entrypoint does (unified + auto-detection dispatches to ``synthesize_to_file``, which also gives the + 0600 output mode). Source precedence: the migrated config when the + migration step ran in this scenario, else the active on-disk config. + When the custom-output service step ran instead, this step is a + pass-through — the service subprocess performs the synthesis. + """ + if getattr(context, "custom_output_path", None): + return + + source = getattr(context, "migrated_config_path", None) or _active_config_path() + scratch = Path(tempfile.mkdtemp(prefix="lcore-e2e-synthesis-")) + context.add_cleanup(lambda: shutil.rmtree(scratch, ignore_errors=True)) + output = scratch / "run.yaml" + + result = _run_cli( + ["src/llama_stack_configuration.py", "-c", str(source), "-o", str(output)] + ) + assert result.returncode == 0 and output.is_file(), ( + f"synthesis CLI failed (rc={result.returncode}) for {source}.\n" + f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" + ) + context.synthesized_run_yaml_path = output + + +@then( + "the synthesized run.yaml parses to the same data as the legacy migration fixture run.yaml" +) +def synthesized_round_trips(context: Context) -> None: + """Assert migrate-then-synthesize reproduces the pair's run.yaml (data equality).""" + synthesized = _load_yaml(_synthesized(context)) + original = _load_yaml( + getattr(context, "migration_pair_run_yaml", None) or Path("run.yaml") + ) + if synthesized != original: + diff = "\n".join( + difflib.unified_diff( + yaml.dump(original, sort_keys=True).splitlines(), + yaml.dump(synthesized, sort_keys=True).splitlines(), + fromfile="pair run.yaml", + tofile="synthesized", + lineterm="", + ) + ) + raise AssertionError(f"round-trip data mismatch:\n{diff}") + + +@then( + "the synthesized run.yaml contains the native_override scalar value for safety.excluded_categories" +) +def synthesized_scalar_override(context: Context) -> None: + """Assert the override's scalar replaced the baseline value at that key (R5).""" + override_value = _native_override(_active_config_path())["safety"][ + "excluded_categories" + ] + synthesized = _load_yaml(_synthesized(context)) + actual = (synthesized.get("safety") or {}).get("excluded_categories") + assert actual == override_value, ( + f"safety.excluded_categories is {actual!r}, expected the " + f"native_override value {override_value!r}" + ) + baseline = _load_yaml(Path("run.yaml")) + baseline_value = (baseline.get("safety") or {}).get("excluded_categories") + assert baseline_value != override_value, ( + "fixture and baseline agree on safety.excluded_categories — the " + "replacement assertion would be vacuous" + ) + + +@then("the synthesized run.yaml contains exactly the native_override list for apis") +def synthesized_list_override(context: Context) -> None: + """Assert the override list replaced the baseline's apis wholesale (R5).""" + override_list = _native_override(_active_config_path())["apis"] + synthesized = _load_yaml(_synthesized(context)) + assert ( + synthesized.get("apis") == override_list + ), f"apis is {synthesized.get('apis')!r}, expected exactly {override_list!r}" + baseline = _load_yaml(Path("run.yaml")) + assert ( + baseline.get("apis") != override_list + ), "fixture and baseline agree on apis — wholesale replacement would be vacuous" + + +@then("the synthesized run.yaml contains ${{env.OPENAI_API_KEY}}") +def synthesized_keeps_env_reference(context: Context) -> None: + """Assert the emitted secret stays an environment reference on disk (R6).""" + content = _synthesized(context).read_text(encoding="utf-8") + assert ( + "${env.OPENAI_API_KEY}" in content + ), "synthesized run.yaml does not carry the ${env.OPENAI_API_KEY} reference" + + +@then("the synthesized run.yaml does not contain the resolved OPENAI_API_KEY value") +def synthesized_no_literal_secret(context: Context) -> None: + """Assert the literal secret value never lands on disk (R6).""" + secret = os.environ.get("OPENAI_API_KEY", "") + assert secret, ( + "OPENAI_API_KEY is not set in the harness environment — the " + "no-literal-secret assertion would be vacuous" + ) + content = _synthesized(context).read_text(encoding="utf-8") + assert ( + secret not in content + ), "synthesized run.yaml contains the resolved OPENAI_API_KEY value" + + +@then("the synthesized run.yaml file permissions are 0600") +def synthesized_permissions(context: Context) -> None: + """Assert the synthesized file is owner-read/write only (R10).""" + mode = stat.S_IMODE(os.stat(_synthesized(context)).st_mode) + assert mode == 0o600, f"synthesized run.yaml mode is {oct(mode)}, expected 0o600" + + +# --------------------------------------------------------------------------- +# --synthesized-config-output (unified-mode-synthesis.feature) +# --------------------------------------------------------------------------- + + +@given( + "lightspeed-stack is started with --synthesized-config-output set to a custom path" +) +def start_with_custom_synthesis_output(context: Context) -> None: + """Launch a short-lived local service with a custom synthesis output path. + + The flag only affects library-mode in-process synthesis, and the running + containers cannot be restarted with different CLI args — so this step + always uses the library-mode variant of the active fixture, copied to a + scratch directory with ``service.port`` rewritten to avoid clashing with + the running stack. The subprocess synthesizes during app startup; the + following Then steps poll for the file, and the process is killed on + scenario cleanup. + """ + active_basename = _active_config_path().name + fixture_basename = Path( + getattr(context, "feature_config", "") + or "lightspeed-stack-unified-providers.yaml" + ).name + base = Path( + getattr(context, "lightspeed_stack_config_directory", "") + or "tests/e2e/configuration" + ) + library_fixture = base / "library-mode" / fixture_basename + if not library_fixture.is_file(): + library_fixture = _config_dir(context) / fixture_basename + assert library_fixture.is_file(), f"no library-mode fixture for {active_basename}" + + scratch = Path(tempfile.mkdtemp(prefix="lcore-e2e-synthout-")) + context.add_cleanup(lambda: shutil.rmtree(scratch, ignore_errors=True)) + + config = _load_yaml(library_fixture) + config.setdefault("service", {})["port"] = 8099 + scratch_config = scratch / "lightspeed-stack.yaml" + with open(scratch_config, "w", encoding="utf-8") as f: + yaml.safe_dump(config, f) + + custom_output = scratch / "custom-run.yaml" + process = subprocess.Popen( # pylint: disable=consider-using-with + [ + sys.executable, + str(Path("src/lightspeed_stack.py").resolve()), + "-c", + str(scratch_config), + "--synthesized-config-output", + str(custom_output), + ], + cwd=str(scratch), + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + context.custom_output_path = custom_output + context.custom_output_scratch = scratch + context.custom_output_process = process + + def _kill() -> None: + if process.poll() is None: + process.kill() + process.wait(timeout=10) + + context.add_cleanup(_kill) + + +@then("the synthesized run.yaml is written to the custom output path") +def custom_output_written(context: Context) -> None: + """Poll for the custom-path synthesis output and validate it parses.""" + custom_output = Path(context.custom_output_path) + process = context.custom_output_process + deadline = time.monotonic() + CUSTOM_OUTPUT_POLL_SECONDS + while time.monotonic() < deadline: + if custom_output.is_file() and custom_output.stat().st_size > 0: + break + if process.poll() is not None and not custom_output.is_file(): + out = process.stdout.read() if process.stdout else "" + raise AssertionError( + f"service exited (rc={process.returncode}) before writing the " + f"custom synthesis output.\n{out[-2000:]}" + ) + time.sleep(0.5) + assert custom_output.is_file(), ( + f"custom synthesis output {custom_output} did not appear within " + f"{CUSTOM_OUTPUT_POLL_SECONDS}s" + ) + assert isinstance(_load_yaml(custom_output), dict) + context.synthesized_run_yaml_path = custom_output + + +@then("the default synthesized run.yaml path does not exist") +def default_output_absent(context: Context) -> None: + """Assert the default synthesis location was not used (override took effect).""" + scratch = Path(context.custom_output_scratch) + default_path = scratch / ".generated" / "run.yaml" + assert ( + not default_path.exists() + ), f"default synthesis path {default_path} exists despite the override" + + +# --------------------------------------------------------------------------- +# Startup logging (unified-mode-synthesis.feature) — mode-aware, see module +# docstring and LCORE-2343 planning decision Q2. +# --------------------------------------------------------------------------- + + +@then("the lightspeed-stack container logs contain synthesized run.yaml") +def container_logs_show_synthesis(context: Context) -> None: + """Assert the container that synthesizes logged the synthesized-config path. + + Library mode: the lightspeed-stack container itself synthesizes in-process + and logs "Using synthesized Llama Stack config at ". Server mode: + synthesis happens in the llama-stack container (entrypoint + config CLI), + which logs the generated-config path — the Gherkin names lightspeed-stack, + but the scenario's intent (R10: the path is logged at startup) can only be + observed on the synthesizing container. Deviation agreed in planning (Q2). + """ + if context.is_library_mode: + container = "lightspeed-stack" + pattern = r"synthesized.*run\.yaml|Using synthesized Llama Stack config" + else: + container = "llama-stack" + pattern = ( + r"Wrote synthesized Llama Stack configuration" + r"|Using generated config:.*run\.yaml" + r"|mode auto-detected" + ) + + result = subprocess.run( + ["docker", "logs", container], + capture_output=True, + text=True, + timeout=60, + check=False, + ) + assert ( + result.returncode == 0 + ), f"docker logs {container} failed: {result.stderr[-500:]}" + logs = result.stdout + result.stderr + assert re.search(pattern, logs), ( + f"{container} logs carry no synthesis-path evidence " + f"(pattern {pattern!r} not found)" + ) From e556cf34ae78a7085f9fa8a4af196fd7cee7f73f Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Tue, 4 Aug 2026 11:13:02 +0200 Subject: [PATCH 057/120] LCORE-2343: gate unified-mode features for Prow and the providers matrix Tag all five unified-mode features @skip-in-prow: the new steps rely on Docker containers and local subprocesses, neither of which exists in the Prow environment (existing convention, handled in before_scenario). Add an @openai-only tag on the two inference.providers boot scenarios and a matching before_scenario skip keyed on E2E_DEFAULT_PROVIDER_OVERRIDE: the providers workflow runs the full unsharded test list against azure/watsonx/bedrock matrices, and the unified-providers fixture hardcodes an openai provider that cannot serve those models' queries. Profile-based fixtures stay provider-agnostic (they consume the CI-materialized run.yaml) and need no gating. --- tests/e2e/features/environment.py | 15 +++++++++++++++ tests/e2e/features/unified-mode-boot.feature | 6 +++--- tests/e2e/features/unified-mode-legacy.feature | 2 +- tests/e2e/features/unified-mode-migration.feature | 2 +- tests/e2e/features/unified-mode-synthesis.feature | 2 +- .../e2e/features/unified-mode-validation.feature | 2 +- 6 files changed, 22 insertions(+), 7 deletions(-) diff --git a/tests/e2e/features/environment.py b/tests/e2e/features/environment.py index 4fc2346fc..02112d40f 100644 --- a/tests/e2e/features/environment.py +++ b/tests/e2e/features/environment.py @@ -242,6 +242,21 @@ def before_scenario(context: Context, scenario: Scenario) -> None: scenario.skip("Skipped in Prow (requires Docker Compose services)") return + # Skip openai-specific scenarios on non-openai provider matrices: the + # providers workflow runs the full test list with E2E_DEFAULT_PROVIDER_OVERRIDE + # set (azure/watsonx/...), and fixtures that hardcode an openai provider + # (e.g. the unified-mode inference.providers fixture) cannot serve queries + # for those models. + provider_override = os.getenv("E2E_DEFAULT_PROVIDER_OVERRIDE", "") + if "openai-only" in scenario.effective_tags and provider_override not in ( + "", + "openai", + ): + scenario.skip( + f"Skipped on provider matrix '{provider_override}' (openai-only fixture)" + ) + return + # In Prow, verify the lightspeed port-forward is alive before each scenario. # Port-forwards can silently die between scenarios (e.g. pod restart, TCP reset). if is_prow_environment(): diff --git a/tests/e2e/features/unified-mode-boot.feature b/tests/e2e/features/unified-mode-boot.feature index 1724dd622..8b6888325 100644 --- a/tests/e2e/features/unified-mode-boot.feature +++ b/tests/e2e/features/unified-mode-boot.feature @@ -1,4 +1,4 @@ -@cfg_unified @skip +@cfg_unified @skip @skip-in-prow Feature: Unified mode configuration boot Background: @@ -10,7 +10,7 @@ Feature: Unified mode configuration boot # --- library mode (@skip-in-server-mode) --- - @skip-in-server-mode + @skip-in-server-mode @openai-only Scenario: Unified config with inference.providers boots and serves requests in library mode Given The service uses the lightspeed-stack-unified-providers.yaml configuration And The service is restarted @@ -62,7 +62,7 @@ Feature: Unified mode configuration boot # --- server mode (@skip-in-library-mode) --- - @skip-in-library-mode + @skip-in-library-mode @openai-only Scenario: Unified config with inference.providers boots and serves requests in server mode Given The service uses the lightspeed-stack-unified-providers.yaml configuration And OGX is restarted diff --git a/tests/e2e/features/unified-mode-legacy.feature b/tests/e2e/features/unified-mode-legacy.feature index 371e1ccb0..f4709d23a 100644 --- a/tests/e2e/features/unified-mode-legacy.feature +++ b/tests/e2e/features/unified-mode-legacy.feature @@ -1,4 +1,4 @@ -@cfg_unified @skip +@cfg_unified @skip @skip-in-prow Feature: Legacy two-file configuration during deprecation window Background: diff --git a/tests/e2e/features/unified-mode-migration.feature b/tests/e2e/features/unified-mode-migration.feature index 7dbca73b1..a701cc769 100644 --- a/tests/e2e/features/unified-mode-migration.feature +++ b/tests/e2e/features/unified-mode-migration.feature @@ -1,4 +1,4 @@ -@cfg_unified @skip +@cfg_unified @skip @skip-in-prow Feature: Legacy to unified configuration migration Background: diff --git a/tests/e2e/features/unified-mode-synthesis.feature b/tests/e2e/features/unified-mode-synthesis.feature index 69f67c44d..62b548678 100644 --- a/tests/e2e/features/unified-mode-synthesis.feature +++ b/tests/e2e/features/unified-mode-synthesis.feature @@ -1,4 +1,4 @@ -@cfg_unified @skip +@cfg_unified @skip @skip-in-prow Feature: Unified mode configuration synthesis Background: diff --git a/tests/e2e/features/unified-mode-validation.feature b/tests/e2e/features/unified-mode-validation.feature index ab7a09038..9f89551ce 100644 --- a/tests/e2e/features/unified-mode-validation.feature +++ b/tests/e2e/features/unified-mode-validation.feature @@ -1,4 +1,4 @@ -@cfg_unified @skip +@cfg_unified @skip @skip-in-prow Feature: Unified mode configuration validation Background: From 3609a1fc0d6616b38d406fe010bc9cc2bb76a1a2 Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Tue, 4 Aug 2026 11:20:09 +0200 Subject: [PATCH 058/120] LCORE-2343: unskip the unified-mode feature files Remove the @skip placeholder tag from the five unified-mode features: the step definitions and fixtures they need now exist. The features keep @skip-in-prow (Docker/subprocess dependencies) and stay in test_list.txt under @e2e_group_2, so CI shards pick them up via 'not @skip and @e2e_group_2' and local runs via --tags=-skip. --- tests/e2e/features/unified-mode-boot.feature | 2 +- tests/e2e/features/unified-mode-legacy.feature | 2 +- tests/e2e/features/unified-mode-migration.feature | 2 +- tests/e2e/features/unified-mode-synthesis.feature | 2 +- tests/e2e/features/unified-mode-validation.feature | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/e2e/features/unified-mode-boot.feature b/tests/e2e/features/unified-mode-boot.feature index 8b6888325..feab84a8d 100644 --- a/tests/e2e/features/unified-mode-boot.feature +++ b/tests/e2e/features/unified-mode-boot.feature @@ -1,4 +1,4 @@ -@cfg_unified @skip @skip-in-prow +@cfg_unified @skip-in-prow Feature: Unified mode configuration boot Background: diff --git a/tests/e2e/features/unified-mode-legacy.feature b/tests/e2e/features/unified-mode-legacy.feature index f4709d23a..c27fe523e 100644 --- a/tests/e2e/features/unified-mode-legacy.feature +++ b/tests/e2e/features/unified-mode-legacy.feature @@ -1,4 +1,4 @@ -@cfg_unified @skip @skip-in-prow +@cfg_unified @skip-in-prow Feature: Legacy two-file configuration during deprecation window Background: diff --git a/tests/e2e/features/unified-mode-migration.feature b/tests/e2e/features/unified-mode-migration.feature index a701cc769..3ace7c80e 100644 --- a/tests/e2e/features/unified-mode-migration.feature +++ b/tests/e2e/features/unified-mode-migration.feature @@ -1,4 +1,4 @@ -@cfg_unified @skip @skip-in-prow +@cfg_unified @skip-in-prow Feature: Legacy to unified configuration migration Background: diff --git a/tests/e2e/features/unified-mode-synthesis.feature b/tests/e2e/features/unified-mode-synthesis.feature index 62b548678..f2ecf8e68 100644 --- a/tests/e2e/features/unified-mode-synthesis.feature +++ b/tests/e2e/features/unified-mode-synthesis.feature @@ -1,4 +1,4 @@ -@cfg_unified @skip @skip-in-prow +@cfg_unified @skip-in-prow Feature: Unified mode configuration synthesis Background: diff --git a/tests/e2e/features/unified-mode-validation.feature b/tests/e2e/features/unified-mode-validation.feature index 9f89551ce..9ad4dae9b 100644 --- a/tests/e2e/features/unified-mode-validation.feature +++ b/tests/e2e/features/unified-mode-validation.feature @@ -1,4 +1,4 @@ -@cfg_unified @skip @skip-in-prow +@cfg_unified @skip-in-prow Feature: Unified mode configuration validation Background: From 8970e070d0232d9f0665b468b9777a19773e5956 Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Tue, 4 Aug 2026 11:34:05 +0200 Subject: [PATCH 059/120] LCORE-2343: close the health-vs-listen race in lightspeed restarts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit restart_container waits on docker health, but docker can report healthy before uvicorn binds the published port — the exact race wait_for_lightspeed_stack_http_ready documents and was, until now, only closed in the proxy steps. The unified-mode boot scenarios are the slowest restarts in the suite (first unified/default-baseline boots) and hit that window reliably: the restart step passed while the following readiness GET got connection-refused. Call the existing HTTP-ready wait from restart_container for the lightspeed-stack container, closing the race for every restarting scenario; when the port is already accepting, the first poll returns immediately. --- tests/e2e/utils/utils.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/e2e/utils/utils.py b/tests/e2e/utils/utils.py index fb35203a3..31d75445a 100644 --- a/tests/e2e/utils/utils.py +++ b/tests/e2e/utils/utils.py @@ -548,6 +548,14 @@ def restart_container(container_name: str) -> None: # that restart the container don't time out. wait_for_container_health(container_name) + # Docker health can report healthy before uvicorn binds the published + # port (the documented race wait_for_lightspeed_stack_http_ready exists + # for). Unified-mode first boots are the slowest restarts in the suite + # and hit that window reliably, so close it here for every restart + # rather than only in the proxy steps. + if container_name == "lightspeed-stack": + wait_for_lightspeed_stack_http_ready() + if container_name == "ogx": from tests.e2e.features.steps.health import ( reset_ogx_disrupt_once_tracking, From 2ff346bb9582a7eeb3333dae518db09df6e3a449 Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Tue, 4 Aug 2026 12:09:05 +0200 Subject: [PATCH 060/120] LCORE-2343: make the migrated e2e config readable by the container user MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --migrate-config writes its output 0600 (R10: migrated files may carry lifted secrets), but the boot scenarios copy that file to the repo root for the container to consume, and the container user cannot read a host-owned 0600 file — the migrated-config boot scenario died on config read. Relax the harness copy to 0644 after a successful migration; the fixture pair is env-reference-only by design, so no secret can leak. --- tests/e2e/features/steps/unified_mode.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/e2e/features/steps/unified_mode.py b/tests/e2e/features/steps/unified_mode.py index 880db4171..f865f913a 100644 --- a/tests/e2e/features/steps/unified_mode.py +++ b/tests/e2e/features/steps/unified_mode.py @@ -186,6 +186,11 @@ def run_migrate_config(context: Context) -> None: f"--migrate-config failed (rc={result.returncode}).\n" f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" ) + # The CLI writes 0600 (R10: migrated files may carry lifted secrets), but + # the container user must be able to read the copy configure_service puts + # at the repo root to boot it. The fixture pair is env-reference-only by + # design, so relaxing the harness copy is safe. + os.chmod(output, 0o644) context.migrated_config_path = output context.migration_pair_run_yaml = pair_run context.add_cleanup(lambda: output.unlink(missing_ok=True)) From 7097491c3bfd52c185342c5ddb52e48ed71ce48c Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Wed, 26 Aug 2026 12:43:58 +0200 Subject: [PATCH 061/120] LCORE-2343: migrate the legacy e2e fixture to the unified rag schema The library-mode legacy fixture declared its BYOK store under a top-level byok_rag key with rag_type, and its tool retrieval sources under rag.tool. LCORE-1426 (commit c1de7f91) refactored RAG configuration into a single rag section: stores moved to rag.byok.stores, retrieval sources to rag.retrieval.tool.sources, and RagStore.rag_type was replaced by RagStore.backend, whose validator accepts only the values in SUPPORTED_RAG_BACKENDS (faiss, pgvector). Configuration models inherit ConfigurationBase with extra=forbid, so after rebasing onto main this fixture raised two extra_forbidden validation errors (rag.tool and byok_rag) and the config could not be loaded at all, failing every legacy library-mode scenario that consumes it. Move the store under rag.byok.stores, replace rag_type: inline::faiss with backend: faiss, and nest the retrieval source list under rag.retrieval.tool.sources. score_multiplier, db_path, embedding_model, embedding_dimension and vector_db_id are unchanged and remain valid RagStore fields. The source id stays e2e-test-docs so the validate_retrieval_sources model validator still resolves it against the declared store. --- .../library-mode/lightspeed-stack-legacy.yaml | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/tests/e2e/configuration/library-mode/lightspeed-stack-legacy.yaml b/tests/e2e/configuration/library-mode/lightspeed-stack-legacy.yaml index 993f0d812..aedf2fc72 100644 --- a/tests/e2e/configuration/library-mode/lightspeed-stack-legacy.yaml +++ b/tests/e2e/configuration/library-mode/lightspeed-stack-legacy.yaml @@ -23,18 +23,20 @@ authentication: inference: default_provider: openai default_model: gpt-4o-mini -byok_rag: - - rag_id: e2e-test-docs - rag_type: inline::faiss - embedding_model: sentence-transformers/all-mpnet-base-v2 - embedding_dimension: 768 - vector_db_id: ${env.FAISS_VECTOR_STORE_ID} - db_path: ${env.KV_RAG_PATH:=~/.llama/storage/rag/kv_store.db} - score_multiplier: 1.0 - rag: - tool: - - e2e-test-docs + byok: + stores: + - rag_id: e2e-test-docs + backend: faiss + embedding_model: sentence-transformers/all-mpnet-base-v2 + embedding_dimension: 768 + vector_db_id: ${env.FAISS_VECTOR_STORE_ID} + db_path: ${env.KV_RAG_PATH:=~/.llama/storage/rag/kv_store.db} + score_multiplier: 1.0 + retrieval: + tool: + sources: + - e2e-test-docs shields: - name: pii-redaction From d4255ed9a75e2447323ac31a6f2fadfef3154bf3 Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Thu, 27 Aug 2026 16:21:06 +0200 Subject: [PATCH 062/120] LCORE-2343: assert the migrate-config 0600 mode instead of relaxing it The migration step ran --migrate-config straight into the fixture directory and then chmod'ed that file to 0644 so the container user could read the copy configure_service places at the repo root. That widened the CLI's own output, which is the artifact R10 governs: migrated configurations may carry secrets lifted out of the legacy run.yaml, and the CLI deliberately writes them owner-only (it logs "mode 0600" when it does). The harness therefore destroyed the property it exists to protect, and no scenario noticed because the 0600 assertion only covers the synthesized run.yaml, not the migrated config. A fixture that ever carries a real secret would have had it published world-readable with nothing failing. Migrate into a scratch directory instead, assert the CLI wrote 0600 there, and publish a separate deliberate 0644 copy under the name the Gherkin steps reference for configure_service to boot. The mode relaxation now applies to a copy that exists only for the harness, the CLI artifact keeps its mode, and the R10 guarantee gains the direct assertion it previously lacked. --- tests/e2e/features/steps/unified_mode.py | 30 +++++++++++++++++++----- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/tests/e2e/features/steps/unified_mode.py b/tests/e2e/features/steps/unified_mode.py index f865f913a..d7699ac84 100644 --- a/tests/e2e/features/steps/unified_mode.py +++ b/tests/e2e/features/steps/unified_mode.py @@ -170,6 +170,13 @@ def run_migrate_config(context: Context) -> None: assert pair_lcs.is_file(), f"missing migration fixture {pair_lcs}" assert pair_run.is_file(), "repo-root run.yaml (harness-materialized) missing" + # Migrate into a scratch directory so the CLI's own artifact keeps the + # mode it was written with and can be asserted on (R10) instead of being + # relaxed in place. + scratch = Path(tempfile.mkdtemp(prefix="lcore-e2e-migrate-")) + context.add_cleanup(lambda: shutil.rmtree(scratch, ignore_errors=True)) + cli_output = scratch / MIGRATED_CONFIG_BASENAME + result = _run_cli( [ "src/lightspeed_stack.py", @@ -179,18 +186,29 @@ def run_migrate_config(context: Context) -> None: "-c", str(pair_lcs), "--migrate-output", - str(output), + str(cli_output), ] ) - assert result.returncode == 0 and output.is_file(), ( + assert result.returncode == 0 and cli_output.is_file(), ( f"--migrate-config failed (rc={result.returncode}).\n" f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" ) - # The CLI writes 0600 (R10: migrated files may carry lifted secrets), but - # the container user must be able to read the copy configure_service puts - # at the repo root to boot it. The fixture pair is env-reference-only by - # design, so relaxing the harness copy is safe. + # R10: migrated files may carry lifted secrets, so the CLI must write them + # owner-only. Assert it here rather than silently relaxing the artifact. + cli_mode = stat.S_IMODE(os.stat(cli_output).st_mode) + assert cli_mode == 0o600, ( + f"--migrate-config wrote {cli_output} with mode {oct(cli_mode)}, " + "expected 0o600 (R10)" + ) + + # configure_service boots a repo-root copy of this file, and the container + # user cannot read a host-owned 0600 file. Publish a deliberate 0644 *copy* + # into the fixture directory for the harness to boot; the CLI artifact + # above keeps its 0600 mode. The fixture pair is env-reference-only by + # design, so nothing secret is widened. + shutil.copyfile(cli_output, output) os.chmod(output, 0o644) + context.migrated_cli_output_path = cli_output context.migrated_config_path = output context.migration_pair_run_yaml = pair_run context.add_cleanup(lambda: output.unlink(missing_ok=True)) From 5e8522f0ce25feaa6f53232a8f54f4ac9ff88986 Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Thu, 27 Aug 2026 16:21:50 +0200 Subject: [PATCH 063/120] LCORE-2343: match the synthesis log assertions to the OGX-renamed messages The startup-logging step matched "Using synthesized Llama Stack config" in library mode and "Wrote synthesized Llama Stack configuration" in server mode. The OGX rename (PRs #2516 and #2547) replaced both: client.py now logs "Using synthesized OGX config at %s" and llama_stack_configuration.py logs "Wrote synthesized OGX configuration to %s (mode 0600)". Neither scenario started failing, which is the reason to fix it now rather than after a real breakage. Library mode kept passing only because the sibling alternative "synthesized.*run\.yaml" incidentally matches the OGX line, the synthesized file being named run.yaml; server mode kept passing on the entrypoint's own "Using generated config:" and "mode auto-detected" echoes. Both scenarios were therefore asserting something other than the message they name, and would have gone silently unprotected the moment the output filename or the entrypoint echoes changed. Point the patterns at the current messages and record in the docstring where each one is emitted, so the next rename has an obvious place to look. --- tests/e2e/features/steps/unified_mode.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/e2e/features/steps/unified_mode.py b/tests/e2e/features/steps/unified_mode.py index d7699ac84..418d5af19 100644 --- a/tests/e2e/features/steps/unified_mode.py +++ b/tests/e2e/features/steps/unified_mode.py @@ -466,19 +466,23 @@ def container_logs_show_synthesis(context: Context) -> None: """Assert the container that synthesizes logged the synthesized-config path. Library mode: the lightspeed-stack container itself synthesizes in-process - and logs "Using synthesized Llama Stack config at ". Server mode: - synthesis happens in the llama-stack container (entrypoint + config CLI), - which logs the generated-config path — the Gherkin names lightspeed-stack, - but the scenario's intent (R10: the path is logged at startup) can only be - observed on the synthesizing container. Deviation agreed in planning (Q2). + and logs "Using synthesized OGX config at ". Server mode: synthesis + happens in the llama-stack container (entrypoint + config CLI), which logs + the generated-config path — the Gherkin names lightspeed-stack, but the + scenario's intent (R10: the path is logged at startup) can only be observed + on the synthesizing container. Deviation agreed in planning (Q2). + + The message text follows the OGX rename (PRs #2516/#2547): client.py logs + "Using synthesized OGX config at %s" and llama_stack_configuration.py logs + "Wrote synthesized OGX configuration to %s (mode 0600)". """ if context.is_library_mode: container = "lightspeed-stack" - pattern = r"synthesized.*run\.yaml|Using synthesized Llama Stack config" + pattern = r"Using synthesized OGX config|synthesized.*run\.yaml" else: container = "llama-stack" pattern = ( - r"Wrote synthesized Llama Stack configuration" + r"Wrote synthesized OGX configuration" r"|Using generated config:.*run\.yaml" r"|mode auto-detected" ) From 5549a521f3a83f11c290261ba7904bd7d9eae6dd Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Thu, 27 Aug 2026 16:22:25 +0200 Subject: [PATCH 064/120] LCORE-2343: name the migration boot scenarios for what they assert Both scenarios were titled "drives byte-identical Llama Stack behavior", but neither compares anything byte for byte: they boot the migrated configuration, assert readiness returns 200, and assert a query returns 200. The byte-level claim belongs to "migrate then synthesize round-trips to the original run.yaml" earlier in the same file, which does compare parsed data. Feature files are read as specification, so a title that overstates its scenario misleads anyone auditing what unified-mode migration is actually covered by. Rename both to "boots and serves queries", which is what the steps verify. No step definition, CI tag filter or test_list entry references either title, so this is a documentation-only change. --- tests/e2e/features/unified-mode-migration.feature | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/features/unified-mode-migration.feature b/tests/e2e/features/unified-mode-migration.feature index 3ace7c80e..bc9565e81 100644 --- a/tests/e2e/features/unified-mode-migration.feature +++ b/tests/e2e/features/unified-mode-migration.feature @@ -23,7 +23,7 @@ Feature: Legacy to unified configuration migration # --- library mode (@skip-in-server-mode) --- @skip-in-server-mode - Scenario: Migrated unified configuration drives byte-identical OGX behavior in library mode + Scenario: Migrated unified configuration boots and serves queries in library mode Given lightspeed-stack --migrate-config is run for the legacy migration fixture pair And The service uses the lightspeed-stack-unified-migrated.yaml configuration And The service is restarted @@ -39,7 +39,7 @@ Feature: Legacy to unified configuration migration # --- server mode (@skip-in-library-mode) --- @skip-in-library-mode - Scenario: Migrated unified configuration drives byte-identical OGX behavior in server mode + Scenario: Migrated unified configuration boots and serves queries in server mode Given lightspeed-stack --migrate-config is run for the legacy migration fixture pair And The service uses the lightspeed-stack-unified-migrated.yaml configuration And OGX is restarted From 7436370de26b0a800e3125dad1cd2896a5ed8e4f Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Thu, 27 Aug 2026 16:55:06 +0200 Subject: [PATCH 065/120] LCORE-2343: drop the trailing blank line from the legacy e2e fixture YAMLlint reports "too many blank lines (1 > 0)" at the end of lightspeed-stack-legacy.yaml, which fails the configured formatting check. Strip the trailing newline so the file ends immediately after its last mapping entry. --- .../e2e/configuration/library-mode/lightspeed-stack-legacy.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/e2e/configuration/library-mode/lightspeed-stack-legacy.yaml b/tests/e2e/configuration/library-mode/lightspeed-stack-legacy.yaml index aedf2fc72..6b4ea3c09 100644 --- a/tests/e2e/configuration/library-mode/lightspeed-stack-legacy.yaml +++ b/tests/e2e/configuration/library-mode/lightspeed-stack-legacy.yaml @@ -45,4 +45,3 @@ shields: rules: - pattern: '\d+' replacement: '[NUM]' - From ca8be648ae9a0e1d81e655ece3850d8a72322151 Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Thu, 27 Aug 2026 16:55:06 +0200 Subject: [PATCH 066/120] LCORE-2343: require a synthesized path in every accepted startup-log message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The startup-log step accepted "mode auto-detected" in server mode and a bare "Using synthesized OGX config" in library mode, neither of which carries a path. scripts/llama-stack-entrypoint.sh echoes "(mode auto-detected)" unconditionally and *before* it runs the config CLI, so a scenario asserting R10 ("the synthesized path is logged at startup") passed even when synthesis had failed outright — the precise failure the assertion exists to catch. Require a non-empty path in every alternative: "Using synthesized OGX config at ", "Wrote synthesized OGX configuration to ", and the entrypoint's "Using generated config: ", which unlike the auto-detect echo is only emitted after a successful generation. Verified against the three real messages, and that the pre-synthesis echo is now rejected. --- tests/e2e/features/steps/unified_mode.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/e2e/features/steps/unified_mode.py b/tests/e2e/features/steps/unified_mode.py index 418d5af19..69bd5b3f7 100644 --- a/tests/e2e/features/steps/unified_mode.py +++ b/tests/e2e/features/steps/unified_mode.py @@ -475,16 +475,20 @@ def container_logs_show_synthesis(context: Context) -> None: The message text follows the OGX rename (PRs #2516/#2547): client.py logs "Using synthesized OGX config at %s" and llama_stack_configuration.py logs "Wrote synthesized OGX configuration to %s (mode 0600)". + + Every accepted pattern must carry a path. The entrypoint echoes "(mode + auto-detected)" *before* synthesis runs and unconditionally, so matching it + would let the scenario pass on a failed synthesis — the opposite of what + R10 asks. "Using generated config: " is only echoed on success. """ if context.is_library_mode: container = "lightspeed-stack" - pattern = r"Using synthesized OGX config|synthesized.*run\.yaml" + pattern = r"Using synthesized OGX config at \S+" else: container = "llama-stack" pattern = ( - r"Wrote synthesized OGX configuration" - r"|Using generated config:.*run\.yaml" - r"|mode auto-detected" + r"Wrote synthesized OGX configuration to \S+" + r"|Using generated config:\s*\S+" ) result = subprocess.run( From 3e38e710f449daa53c6a7baff56141ad611fcf3f Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Thu, 27 Aug 2026 16:55:06 +0200 Subject: [PATCH 067/120] LCORE-2343: bound the HTTP readiness wait with a single monotonic deadline wait_for_lightspeed_stack_http_ready counted attempts rather than tracking wall-clock time, so its real ceiling was the per-request timeout plus the sleeps: 80 * 5s + 79 * 1.5s = 518.5s. The AssertionError reported only the backoff total, "~120s", understating the worst case by a factor of four. That gap matters more since this branch wires the wait into every lightspeed-stack restart rather than only the proxy steps: a container that never binds its port could stall a run for over eight minutes per restart, across the eight call sites of restart_container, while the failure text claimed two. Replace the attempt counter with one monotonic deadline covering both the requests and the sleeps, clamp each request timeout to the time remaining, skip a final sleep that would overrun the budget, and report the attempts and elapsed time actually spent. The default budget is 120s, which is what the old message always claimed the bound was. --- tests/e2e/utils/utils.py | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/tests/e2e/utils/utils.py b/tests/e2e/utils/utils.py index 31d75445a..82b132ec9 100644 --- a/tests/e2e/utils/utils.py +++ b/tests/e2e/utils/utils.py @@ -596,8 +596,9 @@ def restart_lightspeed_stack_service( def wait_for_lightspeed_stack_http_ready( - max_attempts: int = 80, + timeout_s: float = 120.0, delay_s: float = 1.5, + request_timeout_s: float = 5.0, ) -> None: """Block until Lightspeed Stack accepts HTTP on the host-mapped port. @@ -608,10 +609,18 @@ def wait_for_lightspeed_stack_http_ready( Treats HTTP 200 and 401 as success: the process is listening. Auth-enabled configs (e.g. RBAC jwk-token) return 401 on probes without a Bearer token. + Bounded by a single monotonic deadline covering both the requests and the + sleeps, and each request is additionally capped at the time remaining, so + the total wait cannot exceed ``timeout_s``. An attempt-counted loop cannot + give that guarantee: with a per-request timeout the worst case is + ``attempts * request_timeout + (attempts - 1) * delay``, which for the + previous defaults was 518.5s while the failure message reported 120s. + Parameters: ---------- - max_attempts: Maximum GET attempts. + timeout_s: Total wall-clock budget for becoming reachable. delay_s: Sleep between attempts. + request_timeout_s: Per-request timeout, clamped to the time remaining. Raises: ------ AssertionError: If ``/liveness`` does not return an accepted status in time. @@ -621,26 +630,35 @@ def wait_for_lightspeed_stack_http_ready( host = os.getenv("E2E_LSC_HOSTNAME", "localhost") port = os.getenv("E2E_LSC_PORT", "8080") url = f"http://{host}:{port}/liveness" - for attempt in range(max_attempts): + started = time.monotonic() + deadline = started + timeout_s + attempt = 0 + while True: + attempt += 1 + remaining = deadline - time.monotonic() + if remaining <= 0: + break try: - response = requests.get(url, timeout=5) + response = requests.get(url, timeout=min(request_timeout_s, remaining)) if response.status_code in (200, 401): return detail = response.text[:200].replace("\n", " ") print( - f"⏱ HTTP wait LSC {attempt + 1}/{max_attempts} " + f"⏱ HTTP wait LSC attempt {attempt} " f"({url} -> {response.status_code}: {detail})..." ) except requests.RequestException as exc: print( - f"⏱ HTTP wait LSC {attempt + 1}/{max_attempts} " + f"⏱ HTTP wait LSC attempt {attempt} " f"({url} -> {exc.__class__.__name__}: {exc})..." ) - if attempt < max_attempts - 1: - time.sleep(delay_s) + if time.monotonic() + delay_s >= deadline: + break + time.sleep(delay_s) + elapsed = time.monotonic() - started raise AssertionError( f"Lightspeed Stack did not become reachable at {url!r} " - f"after {max_attempts} attempts (~{max_attempts * delay_s:.0f}s)" + f"after {attempt} attempts / {elapsed:.0f}s (budget {timeout_s:.0f}s)" ) From 5288e8c525f510890cdc4c99b82ea5a5be4f26e2 Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Fri, 4 Sep 2026 14:57:54 +0200 Subject: [PATCH 068/120] LCORE-2343: write down the e2e/integration test-layer boundary QE review of the unified-mode step definitions surfaced a rule the repository had been applying by convention but had never written down: an e2e step must not import from, invoke, or shell out to anything under src/. The moment it does, the scenario stops proving what a deployed stack does and starts proving what a checked-out source tree does, which is an integration test. Add a "Choosing the Test Layer: E2E or Integration?" section to docs/testing/e2e_testing.md with the three-layer table, the rule, the "would it run against a container image with no source checkout?" test, and the concrete consequences for configuration validation, migration and synthesis (integration) versus boot and log-evidence scenarios (e2e). Cross-reference it from docs/testing/testing.md, tests/e2e/README.md and the "What to Test" list in tests/integration/README.md, which gains a "CLI contracts" item for repo entrypoints run as subprocesses. tests/e2e/README.md linked docs/e2e_testing.md, which does not exist; the guide lives at docs/testing/e2e_testing.md. Fix the link. --- docs/testing/e2e_testing.md | 48 ++++++++++++++++++++++++++++++++++--- docs/testing/testing.md | 1 + tests/e2e/README.md | 4 +++- tests/integration/README.md | 3 +++ 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/docs/testing/e2e_testing.md b/docs/testing/e2e_testing.md index 50d442bec..0305b8dd4 100644 --- a/docs/testing/e2e_testing.md +++ b/docs/testing/e2e_testing.md @@ -15,8 +15,9 @@ This guide describes how to run, extend, and understand the Lightspeed Core Stac 7. [Configuration Files](#configuration-files) 8. [Feature Files and Steps](#feature-files-and-steps) 9. [Gherkin Keywords in Feature Files](#gherkin-keywords-in-feature-files) -10. [Writing New Scenarios](#writing-new-scenarios) -11. [Troubleshooting](#troubleshooting) +10. [Choosing the Test Layer: E2E or Integration?](#choosing-the-test-layer-e2e-or-integration) +11. [Writing New Scenarios](#writing-new-scenarios) +12. [Troubleshooting](#troubleshooting) --- @@ -336,9 +337,50 @@ Here, **Given** sets state, **When** performs the HTTP call, **Then** and **And* --- +## Choosing the Test Layer: E2E or Integration? + +Before writing a scenario, decide whether it belongs here at all. The suite has +three layers, and the boundary between the top two is strict: + +| Layer | Location | Talks to | May touch `src/`? | +|---|---|---|---| +| Unit | `tests/unit/` | one function or class, everything else mocked | yes | +| Integration | `tests/integration/` (pytest) | real configuration loading, real database, real pipelines in-process; external services (OGX, LLM providers) mocked; repo CLIs as subprocesses | yes | +| E2E | `tests/e2e/` (behave) | a deployed stack, through its public surfaces only: the HTTP API, container lifecycle and logs, configuration files the harness applies | **never** | + +The rule for e2e: **a step definition must not import from, invoke, or shell out +to anything under `src/`.** The moment it does, the scenario stops proving what a +deployed stack does and starts proving what a checked-out source tree does — that +is an integration test, and it belongs in `tests/integration/` as pytest, where it +runs in seconds without Docker. + +A quick test: *could this scenario run unchanged against a container image, with +no source checkout on the machine?* If yes, it is e2e. If it needs +`src/lightspeed_stack.py`, `src/ogx_configuration.py`, a Python import from the +service, or a subprocess of a repo entrypoint, it is integration. + +Typical consequences: + +- Configuration **validation**, **migration** (`--migrate-config`) and run.yaml + **synthesis** are integration concerns: they exercise CLIs and the config + pipeline, not a running service. See `tests/integration/test_unified_synthesis.py` + and `tests/integration/test_unified_mode_cli.py`. +- **Boot** scenarios (apply a config, restart, hit `readiness` and `query`) and + **log-evidence** scenarios (`docker logs `) are e2e: they observe the + deployed stack from outside. +- If a scenario needs a generated artifact as its starting point (for example a + migrated configuration), commit the artifact as a fixture and add an + integration test that guards it against drift, rather than generating it inside + the e2e step. + +The integration side of this boundary is described in +[tests/integration/README.md](../../tests/integration/README.md#what-to-test). + +--- + ## Writing New Scenarios -1. **Choose or add a feature file** under `tests/e2e/features/` and use existing steps where possible. If you add a new file, **add it to `tests/e2e/test_list.txt`** so the suite runs it. +1. **Confirm the scenario is e2e at all** — see [Choosing the Test Layer](#choosing-the-test-layer-e2e-or-integration). Then **choose or add a feature file** under `tests/e2e/features/` and use existing steps where possible. If you add a new file, **add it to `tests/e2e/test_list.txt`** so the suite runs it. 2. **Use tags** for mode-dependent or config-dependent behavior (`@skip-in-library-mode`, `@Authorized`, etc.). **Adding a tag that switches configuration** (e.g. a new feature-level or scenario-level config) usually means you must also add or change a **Lightspeed Stack config** file under `configuration/server-mode/` or `library-mode/` and wire the tag in `environment.py` (e.g. in `before_feature` / `after_feature` or `before_scenario` / `after_scenario`) so the config is applied and the container restarted when the tag is active. 3. **Use placeholders** `{MODEL}` and `{PROVIDER}` in request bodies so the same scenario works with different backends. 4. **Add step definitions** in the appropriate `features/steps/*.py` if you need new steps; reuse `context` for host, port, auth, and responses. diff --git a/docs/testing/testing.md b/docs/testing/testing.md index 4331f1dd8..cee759c36 100644 --- a/docs/testing/testing.md +++ b/docs/testing/testing.md @@ -132,6 +132,7 @@ As specified in Definition of Done, new changes need to be covered by tests. Integration tests are based on the [Pytest framework](https://docs.pytest.org/en/) and code coverage is measured by the plugin [pytest-cov](https://github.com/pytest-dev/pytest-cov). For mocking and patching, the [unittest framework](https://docs.python.org/3/library/unittest.html) is used. * Defined in [tests/integration](https://github.com/lightspeed-core/lightspeed-stack/tree/main/tests/integration) +* **Integration or e2e?** Integration tests may touch `src/` (in-process pipelines, repo CLIs as subprocesses); e2e tests never do. See [Choosing the Test Layer](e2e_testing.md#choosing-the-test-layer-e2e-or-integration). diff --git a/tests/e2e/README.md b/tests/e2e/README.md index cdecbfd1f..e7f88ea80 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -2,8 +2,10 @@ End-to-end tests for the Lightspeed Core Stack REST API (Behave, Gherkin). -**Full guide:** [docs/e2e_testing.md](../../docs/e2e_testing.md) — how to run, environment variables, deployment modes, tags and hooks, Gherkin keywords, configuration, and troubleshooting. +**Full guide:** [docs/testing/e2e_testing.md](../../docs/testing/e2e_testing.md) — how to run, environment variables, deployment modes, tags and hooks, Gherkin keywords, configuration, and troubleshooting. * Tests: `tests/e2e/features/*.feature` * Step definitions: `tests/e2e/features/steps/` * Feature list (run order): `test_list.txt` + +**Not sure a scenario is e2e?** Steps must never touch `src/`; validation, migration and synthesis live in `tests/integration/`. See [Choosing the Test Layer](../../docs/testing/e2e_testing.md#choosing-the-test-layer-e2e-or-integration). diff --git a/tests/integration/README.md b/tests/integration/README.md index 6863e4869..e0949b787 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -219,6 +219,9 @@ Integration tests should verify: 3. **External mocks only** - Mock only external services (OGX, external APIs) 4. **Error handling** - HTTP status codes, error messages 5. **Data flow** - Database persistence, cache updates, etc. +6. **CLI contracts** - Repo entrypoints (`src/lightspeed_stack.py --migrate-config`, `--dump-configuration`, `src/ogx_configuration.py`) run as subprocesses: exit codes, messages, written files and their modes + +Anything that needs a *deployed* stack — HTTP against a running service, container restarts, container logs — is an e2e concern instead. Conversely, e2e steps must never touch `src/`; see [Choosing the Test Layer](../../docs/testing/e2e_testing.md#choosing-the-test-layer-e2e-or-integration). ### What NOT to Test From 5bb26fbe6ef0230d413035827152233ea272448d Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Fri, 4 Sep 2026 14:57:54 +0200 Subject: [PATCH 069/120] LCORE-2343: cover unified-mode validation, migration and synthesis in the integration layer The unified-mode e2e step definitions ran src/lightspeed_stack.py and src/ogx_configuration.py as subprocesses for validation, migration and synthesis assertions. Those are integration concerns (see the test-layer boundary in docs/testing/e2e_testing.md); move them here, alongside the in-process synthesis suite from LCORE-2747 that already covered most of the same ground. tests/integration/test_unified_mode_cli.py (new) runs the real entrypoint from the repository root the way operators and the container entrypoint do: - --dump-configuration exits non-zero and names the problem for the three invalid shapes (inference.providers plus a legacy path, a config block plus a legacy path, config_format_version: legacy on a unified body). - --migrate-config writes the unified file owner-only (R10), with the run.yaml carried as native_override and library_client_config_path dropped, and the result synthesizes back to the pair's run.yaml data. - The committed lightspeed-stack-unified-migrated.yaml e2e fixtures match today's CLI output for both modes, so the migration boot scenarios can consume a committed artifact instead of generating one in a step; the docstring carries the regeneration command. tests/integration/test_unified_synthesis.py gains the three assertions the e2e scenarios had and this file lacked: emitted secrets stay ${env.NAME} references on disk (R6), config_format_version: legacy on a unified-shaped body fails the real load (R11), and LIGHTSPEED_STACK_SYNTHESIZED_CONFIG_PATH (set from --synthesized-config-output) redirects library-mode synthesis and leaves the default path untouched. native_override replacement (R5), the 0600 output mode (R10) and the migrate-then-synthesize round trip were already covered and are not duplicated. The CLI tests take their inputs from tests/configuration/unified-mode/, the integration fixture tree, with the legacy path pointed at tests/configuration/run.yaml so the captured failure is the intended cross-field error rather than a missing file. --- ...speed-stack-invalid-config-and-legacy.yaml | 25 +++ ...ed-stack-invalid-providers-and-legacy.yaml | 31 +++ ...k-invalid-version-legacy-unified-body.yaml | 31 +++ ...lightspeed-stack-legacy-for-migration.yaml | 23 +++ tests/integration/test_unified_mode_cli.py | 176 ++++++++++++++++++ tests/integration/test_unified_synthesis.py | 80 ++++++++ 6 files changed, 366 insertions(+) create mode 100644 tests/configuration/unified-mode/lightspeed-stack-invalid-config-and-legacy.yaml create mode 100644 tests/configuration/unified-mode/lightspeed-stack-invalid-providers-and-legacy.yaml create mode 100644 tests/configuration/unified-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml create mode 100644 tests/configuration/unified-mode/lightspeed-stack-legacy-for-migration.yaml create mode 100644 tests/integration/test_unified_mode_cli.py diff --git a/tests/configuration/unified-mode/lightspeed-stack-invalid-config-and-legacy.yaml b/tests/configuration/unified-mode/lightspeed-stack-invalid-config-and-legacy.yaml new file mode 100644 index 000000000..a33693698 --- /dev/null +++ b/tests/configuration/unified-mode/lightspeed-stack-invalid-config-and-legacy.yaml @@ -0,0 +1,25 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + config: + profile: tests/configuration/run.yaml + # INVALID: config block plus the legacy path (mutual exclusion, R3) + library_client_config_path: tests/configuration/run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/configuration/unified-mode/lightspeed-stack-invalid-providers-and-legacy.yaml b/tests/configuration/unified-mode/lightspeed-stack-invalid-providers-and-legacy.yaml new file mode 100644 index 000000000..19c299ab9 --- /dev/null +++ b/tests/configuration/unified-mode/lightspeed-stack-invalid-providers-and-legacy.yaml @@ -0,0 +1,31 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + # INVALID: synthesis input plus the legacy path (mutual exclusion, R3) + library_client_config_path: tests/configuration/run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini + # Unified synthesis input (Decision S5): the high-level provider entry + # replaces the default baseline's openai provider by id at synthesis time. + providers: + - type: openai + id: openai + api_key_env: OPENAI_API_KEY + allowed_models: + - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} diff --git a/tests/configuration/unified-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml b/tests/configuration/unified-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml new file mode 100644 index 000000000..9ae7b389d --- /dev/null +++ b/tests/configuration/unified-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml @@ -0,0 +1,31 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini + # Unified synthesis input (Decision S5): the high-level provider entry + # replaces the default baseline's openai provider by id at synthesis time. + providers: + - type: openai + id: openai + api_key_env: OPENAI_API_KEY + allowed_models: + - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} +# INVALID: explicit legacy marker on a unified-shaped body (R11, LCORE-2872) +config_format_version: legacy diff --git a/tests/configuration/unified-mode/lightspeed-stack-legacy-for-migration.yaml b/tests/configuration/unified-mode/lightspeed-stack-legacy-for-migration.yaml new file mode 100644 index 000000000..bf4bfe96c --- /dev/null +++ b/tests/configuration/unified-mode/lightspeed-stack-legacy-for-migration.yaml @@ -0,0 +1,23 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + # Legacy two-file shape: external run.yaml, no synthesis input + library_client_config_path: tests/configuration/run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/integration/test_unified_mode_cli.py b/tests/integration/test_unified_mode_cli.py new file mode 100644 index 000000000..09a2b735e --- /dev/null +++ b/tests/integration/test_unified_mode_cli.py @@ -0,0 +1,176 @@ +"""Integration tests for the unified-mode CLI contracts (LCORE-2343). + +These cover the surfaces the unified-mode e2e features used to exercise by +shelling out to ``src/`` — which e2e steps must not do (see +``docs/testing/e2e_testing.md``, "Choosing the Test Layer"): configuration +validation through ``lightspeed_stack.py --dump-configuration``, +legacy-to-unified migration through ``--migrate-config``, and the committed +migrated e2e fixture that replaced the migration step in +``unified-mode-migration.feature``. + +Everything here runs the real entrypoint as a subprocess from the repository +root, the way operators and the container entrypoint invoke it; the in-process +half of the same pipeline lives in ``test_unified_synthesis.py``. +""" + +import os +import stat +import subprocess +import sys +from pathlib import Path +from typing import Any + +import pytest +import yaml + +from ogx_configuration import synthesize_configuration + +_REPO_ROOT = Path(__file__).resolve().parents[2] +_ENTRYPOINT = _REPO_ROOT / "src" / "lightspeed_stack.py" +_FIXTURES = _REPO_ROOT / "tests" / "configuration" / "unified-mode" +_E2E_FIXTURES = _REPO_ROOT / "tests" / "e2e" / "configuration" / "unified-mode" +# The run.yaml the e2e harness materializes at the repo root in CI; the +# committed migrated e2e fixtures were generated against it. +_E2E_RUN_YAML = _REPO_ROOT / "tests" / "e2e" / "configs" / "run-ci.yaml" +_MIGRATED_FIXTURE = "lightspeed-stack-unified-migrated.yaml" +_LEGACY_PAIR_FIXTURE = "lightspeed-stack-legacy-for-migration.yaml" +_CLI_TIMEOUT_SECONDS = 120 + + +def _run_cli(*args: str) -> subprocess.CompletedProcess[str]: + """Run the service entrypoint as a subprocess from the repo root, never raising.""" + return subprocess.run( + [sys.executable, str(_ENTRYPOINT), *args], + cwd=_REPO_ROOT, + capture_output=True, + text=True, + timeout=_CLI_TIMEOUT_SECONDS, + check=False, + ) + + +def _load_yaml(path: Path) -> Any: + """Parse a YAML file.""" + return yaml.safe_load(path.read_text(encoding="utf-8")) + + +def _migrate(lcs_config: Path, run_yaml: Path, output: Path) -> None: + """Run ``--migrate-config`` for a legacy pair and assert it succeeded.""" + result = _run_cli( + "--migrate-config", + "--run-yaml", + str(run_yaml), + "-c", + str(lcs_config), + "--migrate-output", + str(output), + ) + assert result.returncode == 0 and output.is_file(), ( + f"--migrate-config failed (rc={result.returncode}).\n" + f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" + ) + + +# --------------------------------------------------------------------------- +# Validation through the CLI (R3 mutual exclusion, R11 format-version marker) +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + ("fixture", "expected_message"), + [ + pytest.param( + "lightspeed-stack-invalid-providers-and-legacy.yaml", + "--migrate-config", + id="inference-providers-plus-legacy-path", + ), + pytest.param( + "lightspeed-stack-invalid-config-and-legacy.yaml", + "--migrate-config", + id="config-block-plus-legacy-path", + ), + pytest.param( + "lightspeed-stack-invalid-version-legacy-unified-body.yaml", + "config_format_version", + id="legacy-version-marker-on-unified-body", + ), + ], +) +def test_cli_rejects_invalid_configuration(fixture: str, expected_message: str) -> None: + """``--dump-configuration`` exits non-zero and names the problem. + + ``main()`` loads (and thereby validates) the configuration before any dump + handling, so a failed cross-field validation surfaces as a non-zero exit + with the Pydantic message on stderr. The fixtures point their legacy path + at an existing run.yaml so the captured failure is the intended + cross-field error, not a file-not-found. + """ + result = _run_cli("--dump-configuration", "-c", str(_FIXTURES / fixture)) + output = result.stdout + result.stderr + + assert result.returncode != 0, ( + f"expected {fixture} to fail validation, but the load succeeded. " + f"Output:\n{output}" + ) + assert expected_message in output, ( + f"validation failure for {fixture} does not mention " + f"{expected_message!r}. Full output:\n{output}" + ) + + +# --------------------------------------------------------------------------- +# --migrate-config contract +# --------------------------------------------------------------------------- + + +def test_cli_migrate_config_writes_unified_owner_only(tmp_path: Path) -> None: + """``--migrate-config`` emits a unified file, owner-only, that round-trips. + + The output carries the run.yaml as ``native_override`` and drops the legacy + ``library_client_config_path``; it is written 0600 because migrated files + may carry lifted secrets (R10); and synthesizing it reproduces the pair's + run.yaml data (migrate-then-synthesize round trip). + """ + output = tmp_path / "unified.yaml" + _migrate(_FIXTURES / _LEGACY_PAIR_FIXTURE, _E2E_RUN_YAML, output) + + mode = stat.S_IMODE(os.stat(output).st_mode) + assert mode == 0o600, f"migrated file mode is {oct(mode)}, expected 0o600" + + text = output.read_text(encoding="utf-8") + migrated = yaml.safe_load(text) + assert migrated["ogx"]["config"][ + "native_override" + ], "migrated config carries no native_override" + assert "library_client_config_path" not in text + + synthesized = synthesize_configuration(migrated, config_file_dir=str(tmp_path)) + assert synthesized == _load_yaml(_E2E_RUN_YAML) + + +@pytest.mark.parametrize("mode", ["library-mode", "server-mode"]) +def test_committed_migrated_fixture_matches_cli_output( + tmp_path: Path, mode: str +) -> None: + """The committed migrated e2e fixture is exactly what the CLI produces today. + + ``unified-mode-migration.feature`` boots + ``tests/e2e/configuration/unified-mode//lightspeed-stack-unified-migrated.yaml`` + instead of generating it in a step (e2e steps never run ``src/`` CLIs). + This guard fails the moment ``--migrate-config`` output drifts from the + committed file. To refresh the fixture, run from the repo root:: + + uv run python src/lightspeed_stack.py --migrate-config \\ + --run-yaml tests/e2e/configs/run-ci.yaml \\ + -c tests/e2e/configuration/unified-mode//lightspeed-stack-legacy-for-migration.yaml \\ + --migrate-output tests/e2e/configuration/unified-mode//lightspeed-stack-unified-migrated.yaml + chmod 644 tests/e2e/configuration/unified-mode//lightspeed-stack-unified-migrated.yaml + """ + output = tmp_path / "migrated.yaml" + _migrate(_E2E_FIXTURES / mode / _LEGACY_PAIR_FIXTURE, _E2E_RUN_YAML, output) + + committed = _E2E_FIXTURES / mode / _MIGRATED_FIXTURE + assert _load_yaml(output) == _load_yaml(committed), ( + f"{committed} no longer matches --migrate-config output; regenerate it " + "(see this test's docstring)" + ) diff --git a/tests/integration/test_unified_synthesis.py b/tests/integration/test_unified_synthesis.py index fa7c8d1e6..b5a7f6105 100644 --- a/tests/integration/test_unified_synthesis.py +++ b/tests/integration/test_unified_synthesis.py @@ -23,6 +23,8 @@ import yaml from pydantic import ValidationError +import constants +from client.ogx import AsyncOgxClientHolder from configuration import configuration from ogx_configuration import ( CONDITIONAL_OPENAI_PROVIDER_ID, @@ -522,3 +524,81 @@ def test_load_accepts_minimal_unified_config(tmp_path: Path) -> None: loaded = configuration.configuration assert loaded.ogx.config is None assert loaded.inference.providers[0].type == "openai" + + +# --------------------------------------------------------------------------- +# R6: emitted secrets stay environment references on disk +# --------------------------------------------------------------------------- + + +def test_synthesized_secrets_stay_env_references( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A provider's ``api_key_env`` lands on disk as ``${env.NAME}``, never resolved. + + The synthesizer must emit the environment reference for OGX to resolve at + its own startup; the literal secret value must never be written (R6). + """ + secret = "sk-resolved-secret-that-must-not-land-on-disk" + monkeypatch.setenv("OPENAI_API_KEY", secret) + lcs_dict = _base_config_dict() + lcs_dict["ogx"] = {"use_as_library_client": True} + lcs_dict["inference"] = { + "providers": [{"type": "openai", "api_key_env": "OPENAI_API_KEY"}] + } + _, out_path = _load_and_synthesize(tmp_path, lcs_dict) + + text = out_path.read_text(encoding="utf-8") + assert "${env.OPENAI_API_KEY}" in text + assert secret not in text + + +# --------------------------------------------------------------------------- +# R11: explicit config_format_version must agree with the detected shape +# --------------------------------------------------------------------------- + + +def test_load_rejects_legacy_version_marker_on_unified_body(tmp_path: Path) -> None: + """``config_format_version: legacy`` on a unified-shaped body fails the real load.""" + lcs_dict = _base_config_dict() + lcs_dict["ogx"] = {"use_as_library_client": True} + lcs_dict["inference"] = { + "providers": [{"type": "openai", "api_key_env": "OPENAI_API_KEY"}] + } + lcs_dict["config_format_version"] = "legacy" + cfg_path = _write_yaml(tmp_path / "lightspeed-stack.yaml", lcs_dict) + with pytest.raises(ValidationError, match="config_format_version"): + configuration.load_configuration(str(cfg_path)) + + +# --------------------------------------------------------------------------- +# --synthesized-config-output: the override the workers honour +# --------------------------------------------------------------------------- + + +def test_synthesized_config_output_override( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """The synthesized-config-output override redirects library-mode synthesis. + + ``--synthesized-config-output`` reaches the uvicorn workers as + ``LIGHTSPEED_STACK_SYNTHESIZED_CONFIG_PATH``; the client holder must write + the synthesized run.yaml there and leave the default location untouched. + """ + lcs_dict = _base_config_dict() + lcs_dict["ogx"] = { + "use_as_library_client": True, + "config": {"baseline": "empty", "native_override": {"version": 2}}, + } + cfg_path = _write_yaml(tmp_path / "lightspeed-stack.yaml", lcs_dict) + custom_output = tmp_path / "custom-run.yaml" + monkeypatch.chdir(tmp_path) + monkeypatch.setenv(constants.CONFIG_PATH_ENV_VAR, str(cfg_path)) + monkeypatch.setenv(constants.SYNTHESIZED_CONFIG_PATH_ENV_VAR, str(custom_output)) + + # pylint: disable-next=protected-access + written = AsyncOgxClientHolder()._synthesize_library_config() + + assert Path(written) == custom_output + assert isinstance(yaml.safe_load(custom_output.read_text(encoding="utf-8")), dict) + assert not (tmp_path / constants.DEFAULT_SYNTHESIZED_CONFIG_PATH).exists() From 9de142b1bea5af1e336e4f9a575949ab469175a5 Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Fri, 4 Sep 2026 14:57:54 +0200 Subject: [PATCH 070/120] LCORE-2343: keep only deployed-stack scenarios in the unified-mode e2e features Apply the test-layer boundary: e2e steps observe the deployed stack from outside and never touch src/. Of the 24 unified-mode scenarios, 14 did that already (boot, legacy, startup-log evidence) and stay as they are; 10 exercised repo CLIs and now live in tests/integration (previous commit). - unified-mode-validation.feature is removed: all three scenarios are CLI contract tests. Dropped from test_list.txt. - unified-mode-synthesis.feature keeps the two startup-log scenarios, the one thing only a running stack can show; native_override replacement, env-reference secrets, the 0600 mode and --synthesized-config-output are covered in-process. - unified-mode-migration.feature keeps the two "migrated configuration boots and serves queries" scenarios. They used to generate their input by running --migrate-config inside a Given; they now boot a committed lightspeed-stack-unified-migrated.yaml fixture per mode, generated once from lightspeed-stack-legacy-for-migration.yaml and tests/e2e/configs/run-ci.yaml and guarded against CLI drift by test_unified_mode_cli.py. The fixture inlines the openai run-ci.yaml, so both scenarios carry @openai-only like the other provider-specific boots. The .gitignore entry for the generated file goes away with the step. - The migrate-then-synthesize round-trip scenario is dropped as a duplicate of the in-process test that already existed. steps/unified_mode.py shrinks from 508 lines and 16 patterns to the single mode-aware container-log step. The never-booted fixtures (three invalid shapes, two native_override shapes, both mode variants) leave the e2e tree; the fixtures README documents what remains and why. Gherkin edits are limited to removing scenarios and the generating Given, plus the @openai-only tags; no surviving step was weakened. --- .gitignore | 2 - .../e2e/configuration/unified-mode/README.md | 17 +- ...speed-stack-invalid-config-and-legacy.yaml | 25 - ...ed-stack-invalid-providers-and-legacy.yaml | 31 -- ...k-invalid-version-legacy-unified-body.yaml | 31 -- .../lightspeed-stack-unified-migrated.yaml | 126 +++++ ...ed-stack-unified-native-override-list.yaml | 29 -- ...-stack-unified-native-override-scalar.yaml | 28 -- ...speed-stack-invalid-config-and-legacy.yaml | 27 - ...ed-stack-invalid-providers-and-legacy.yaml | 33 -- ...k-invalid-version-legacy-unified-body.yaml | 33 -- .../lightspeed-stack-unified-migrated.yaml | 128 +++++ ...ed-stack-unified-native-override-list.yaml | 31 -- ...-stack-unified-native-override-scalar.yaml | 30 -- tests/e2e/features/steps/unified_mode.py | 472 +----------------- .../features/unified-mode-migration.feature | 29 +- .../features/unified-mode-synthesis.feature | 39 +- .../features/unified-mode-validation.feature | 26 - tests/e2e/test_list.txt | 1 - 19 files changed, 297 insertions(+), 841 deletions(-) delete mode 100644 tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-config-and-legacy.yaml delete mode 100644 tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-providers-and-legacy.yaml delete mode 100644 tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml create mode 100644 tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-migrated.yaml delete mode 100644 tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-native-override-list.yaml delete mode 100644 tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-native-override-scalar.yaml delete mode 100644 tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-config-and-legacy.yaml delete mode 100644 tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-providers-and-legacy.yaml delete mode 100644 tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml create mode 100644 tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-migrated.yaml delete mode 100644 tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-native-override-list.yaml delete mode 100644 tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-native-override-scalar.yaml delete mode 100644 tests/e2e/features/unified-mode-validation.feature diff --git a/.gitignore b/.gitignore index 3225ab2a9..0d5d1e1c1 100644 --- a/.gitignore +++ b/.gitignore @@ -203,5 +203,3 @@ local-run.yaml # Per-developer feature design overrides (see docs/contributing/feature-design.config) .feature-design.config.local -# Generated at e2e test time by the unified-mode --migrate-config step -tests/e2e/configuration/unified-mode/**/lightspeed-stack-unified-migrated.yaml diff --git a/tests/e2e/configuration/unified-mode/README.md b/tests/e2e/configuration/unified-mode/README.md index 79eaaabe0..98131ba4f 100644 --- a/tests/e2e/configuration/unified-mode/README.md +++ b/tests/e2e/configuration/unified-mode/README.md @@ -1,6 +1,6 @@ # Unified-mode e2e configuration fixtures -Fixtures for the five `unified-mode-*.feature` files (LCORE-2341/LCORE-2343). +Fixtures for the `unified-mode-*.feature` files (LCORE-2341/LCORE-2343). Same layout as the parent directory: `library-mode/` and `server-mode/` variants differing only in the `llama_stack` block; the harness resolves `//` via the standard `configure_service` logic. @@ -9,16 +9,17 @@ All profile-based fixtures reference `run.yaml` — the repo-root copy the CI harness materializes from `tests/e2e/configs/run-.yaml` — so they stay provider-agnostic across the providers matrix. +Only bootable fixtures live here. The validation-only and synthesis-only +inputs (invalid configs, `native_override` shapes) belong to the integration +layer — `tests/configuration/unified-mode/` and +`tests/integration/test_unified_synthesis.py` — because e2e steps never run +`src/` CLIs (see `docs/testing/e2e_testing.md`, "Choosing the Test Layer"). + | Fixture | Purpose | |---|---| | `lightspeed-stack-unified-providers.yaml` | Minimal unified config driven only by top-level `inference.providers` (default baseline, R1/S5). openai-specific — used by `@openai-only` scenarios. | | `lightspeed-stack-unified-config-only.yaml` | Unified config driven only by `llama_stack.config` (`profile: run.yaml`, R1). | | `lightspeed-stack-unified-relative-profile.yaml` | Same shape as config-only; exists to pin R8 (relative `profile:` resolves against the config file's directory) as a distinct intent. | | `lightspeed-stack-unified-absolute-profile.yaml` | `profile:` as a container-absolute path (differs per mode subdir). | -| `lightspeed-stack-unified-native-override-scalar.yaml` | `native_override` replaces an overlapping scalar key (R5). Synthesis-only; never booted. | -| `lightspeed-stack-unified-native-override-list.yaml` | `native_override` replaces an overlapping list wholesale (R5). Synthesis-only; never booted. | -| `lightspeed-stack-invalid-providers-and-legacy.yaml` | INVALID: `inference.providers` + `library_client_config_path` (mutual exclusion, R3). Validation-only. | -| `lightspeed-stack-invalid-config-and-legacy.yaml` | INVALID: `llama_stack.config` + `library_client_config_path` (R3). Validation-only. | -| `lightspeed-stack-invalid-version-legacy-unified-body.yaml` | INVALID: `config_format_version: legacy` on a unified-shaped body (R11, LCORE-2872). Validation-only. | -| `lightspeed-stack-legacy-for-migration.yaml` | Legacy half of "the legacy migration fixture pair"; paired with the repo-root `run.yaml`. Deliberately free of enrichment sections so migrate→synthesize round-trips losslessly (see LCORE-3370). | -| `lightspeed-stack-unified-migrated.yaml` | Generated at test time by the `--migrate-config` step; gitignored and cleaned up after each scenario. | +| `lightspeed-stack-legacy-for-migration.yaml` | Legacy half of the migration fixture pair; paired with `tests/e2e/configs/run-ci.yaml`. Deliberately free of enrichment sections so migrate→synthesize round-trips losslessly (see LCORE-3370). Input to the drift guard below; never booted. | +| `lightspeed-stack-unified-migrated.yaml` | **Committed** output of `--migrate-config` for the pair above. Booted by `unified-mode-migration.feature` (`@openai-only`: it inlines the openai run-ci.yaml). `tests/integration/test_unified_mode_cli.py::test_committed_migrated_fixture_matches_cli_output` fails when the CLI output drifts; its docstring has the regeneration command. | diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-config-and-legacy.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-config-and-legacy.yaml deleted file mode 100644 index bdb8af911..000000000 --- a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-config-and-legacy.yaml +++ /dev/null @@ -1,25 +0,0 @@ -name: Lightspeed Core Service (LCS) -service: - host: 0.0.0.0 - port: 8080 - auth_enabled: false - workers: 1 - color_log: true - access_log: true -llama_stack: - # Library mode - embeds the stack in-process - use_as_library_client: true - config: - profile: run.yaml - # INVALID: config block plus the legacy path (mutual exclusion, R3) - library_client_config_path: run.yaml -user_data_collection: - feedback_enabled: true - feedback_storage: "/tmp/data/feedback" - transcripts_enabled: true - transcripts_storage: "/tmp/data/transcripts" -authentication: - module: "noop" -inference: - default_provider: openai - default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-providers-and-legacy.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-providers-and-legacy.yaml deleted file mode 100644 index 124fd453c..000000000 --- a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-providers-and-legacy.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: Lightspeed Core Service (LCS) -service: - host: 0.0.0.0 - port: 8080 - auth_enabled: false - workers: 1 - color_log: true - access_log: true -llama_stack: - # Library mode - embeds the stack in-process - use_as_library_client: true - # INVALID: synthesis input plus the legacy path (mutual exclusion, R3) - library_client_config_path: run.yaml -user_data_collection: - feedback_enabled: true - feedback_storage: "/tmp/data/feedback" - transcripts_enabled: true - transcripts_storage: "/tmp/data/transcripts" -authentication: - module: "noop" -inference: - default_provider: openai - default_model: gpt-4o-mini - # Unified synthesis input (Decision S5): the high-level provider entry - # replaces the default baseline's openai provider by id at synthesis time. - providers: - - type: openai - id: openai - api_key_env: OPENAI_API_KEY - allowed_models: - - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml deleted file mode 100644 index 9ae7b389d..000000000 --- a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: Lightspeed Core Service (LCS) -service: - host: 0.0.0.0 - port: 8080 - auth_enabled: false - workers: 1 - color_log: true - access_log: true -llama_stack: - # Library mode - embeds the stack in-process - use_as_library_client: true -user_data_collection: - feedback_enabled: true - feedback_storage: "/tmp/data/feedback" - transcripts_enabled: true - transcripts_storage: "/tmp/data/transcripts" -authentication: - module: "noop" -inference: - default_provider: openai - default_model: gpt-4o-mini - # Unified synthesis input (Decision S5): the high-level provider entry - # replaces the default baseline's openai provider by id at synthesis time. - providers: - - type: openai - id: openai - api_key_env: OPENAI_API_KEY - allowed_models: - - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} -# INVALID: explicit legacy marker on a unified-shaped body (R11, LCORE-2872) -config_format_version: legacy diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-migrated.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-migrated.yaml new file mode 100644 index 000000000..576b1950a --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-migrated.yaml @@ -0,0 +1,126 @@ +authentication: + module: noop +inference: + default_model: gpt-4o-mini + default_provider: openai +name: Lightspeed Core Service (LCS) +ogx: + config: + baseline: empty + native_override: + apis: + - responses + - batches + - files + - inference + - tool_runtime + - conversations + - vector_io + distro_name: starter + providers: + batches: + - config: + sqlstore: + backend: sql_default + table_name: batches + provider_id: reference + provider_type: inline::reference + files: + - config: + metadata_store: + backend: sql_default + table_name: files_metadata + storage_dir: ~/.llama/storage/files + provider_id: meta-reference-files + provider_type: inline::localfs + inference: + - config: + allowed_models: + - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} + api_key: ${env.OPENAI_API_KEY} + provider_id: openai + provider_type: remote::openai + - config: {} + provider_id: sentence-transformers + provider_type: inline::sentence-transformers + responses: + - config: + persistence: + responses: + backend: sql_default + table_name: agents_responses + provider_id: builtin + provider_type: inline::builtin + tool_runtime: + - config: {} + provider_id: file-search + provider_type: inline::file-search + - config: {} + provider_id: model-context-protocol + provider_type: remote::model-context-protocol + vector_io: + - config: + persistence: + backend: kv_rag + namespace: vector_io::faiss + provider_id: faiss + provider_type: inline::faiss + registered_resources: + models: + - metadata: + embedding_dimension: 768 + model_id: all-mpnet-base-v2 + model_type: embedding + provider_id: sentence-transformers + provider_model_id: all-mpnet-base-v2 + vector_stores: [] + server: + port: 8321 + storage: + backends: + kv_default: + db_path: ${env.KV_STORE_PATH:=~/.llama/storage/kv_store.db} + type: kv_sqlite + kv_rag: + db_path: ${env.KV_RAG_PATH:=~/.llama/storage/rag/kv_store.db} + type: kv_sqlite + sql_default: + db_path: ${env.SQL_STORE_PATH:=~/.llama/storage/sql_store.db} + type: sql_sqlite + stores: + connectors: + backend: sql_default + table_name: connectors + conversations: + backend: sql_default + table_name: openai_conversations + inference: + backend: sql_default + max_write_queue_size: 10000 + num_writers: 4 + table_name: inference_store + metadata: + backend: kv_default + namespace: registry + prompts: + backend: sql_default + table_name: prompts + vector_stores: + default_embedding_model: + model_id: all-mpnet-base-v2 + provider_id: sentence-transformers + default_provider_id: faiss + version: 2 + use_as_library_client: true +service: + access_log: true + auth_enabled: false + color_log: true + host: 0.0.0.0 + port: 8080 + workers: 1 +user_data_collection: + feedback_enabled: true + feedback_storage: /tmp/data/feedback + transcripts_enabled: true + transcripts_storage: /tmp/data/transcripts diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-native-override-list.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-native-override-list.yaml deleted file mode 100644 index 403cc7a00..000000000 --- a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-native-override-list.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: Lightspeed Core Service (LCS) -service: - host: 0.0.0.0 - port: 8080 - auth_enabled: false - workers: 1 - color_log: true - access_log: true -llama_stack: - # Library mode - embeds the stack in-process - use_as_library_client: true - config: - profile: run.yaml - # R5: lists replace wholesale - the synthesized apis must equal exactly - # this list, not a merge with the baseline's (never booted - synthesis only) - native_override: - apis: - - inference - - tool_runtime -user_data_collection: - feedback_enabled: true - feedback_storage: "/tmp/data/feedback" - transcripts_enabled: true - transcripts_storage: "/tmp/data/transcripts" -authentication: - module: "noop" -inference: - default_provider: openai - default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-native-override-scalar.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-native-override-scalar.yaml deleted file mode 100644 index 02f67f028..000000000 --- a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-native-override-scalar.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: Lightspeed Core Service (LCS) -service: - host: 0.0.0.0 - port: 8080 - auth_enabled: false - workers: 1 - color_log: true - access_log: true -llama_stack: - # Library mode - embeds the stack in-process - use_as_library_client: true - config: - profile: run.yaml - # R5: the raw escape hatch wins; this scalar replaces the baseline's - # safety.excluded_categories value wholesale (never booted - synthesis only) - native_override: - safety: - excluded_categories: unified-override-marker -user_data_collection: - feedback_enabled: true - feedback_storage: "/tmp/data/feedback" - transcripts_enabled: true - transcripts_storage: "/tmp/data/transcripts" -authentication: - module: "noop" -inference: - default_provider: openai - default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-config-and-legacy.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-config-and-legacy.yaml deleted file mode 100644 index 34dbfb06f..000000000 --- a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-config-and-legacy.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: Lightspeed Core Service (LCS) -service: - host: 0.0.0.0 - port: 8080 - auth_enabled: false - workers: 1 - color_log: true - access_log: true -llama_stack: - # Server mode - connects to the separate llama-stack service - use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 - api_key: xyzzy - config: - profile: run.yaml - # INVALID: config block plus the legacy path (mutual exclusion, R3) - library_client_config_path: run.yaml -user_data_collection: - feedback_enabled: true - feedback_storage: "/tmp/data/feedback" - transcripts_enabled: true - transcripts_storage: "/tmp/data/transcripts" -authentication: - module: "noop" -inference: - default_provider: openai - default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-providers-and-legacy.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-providers-and-legacy.yaml deleted file mode 100644 index 6cbd5f50a..000000000 --- a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-providers-and-legacy.yaml +++ /dev/null @@ -1,33 +0,0 @@ -name: Lightspeed Core Service (LCS) -service: - host: 0.0.0.0 - port: 8080 - auth_enabled: false - workers: 1 - color_log: true - access_log: true -llama_stack: - # Server mode - connects to the separate llama-stack service - use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 - api_key: xyzzy - # INVALID: synthesis input plus the legacy path (mutual exclusion, R3) - library_client_config_path: run.yaml -user_data_collection: - feedback_enabled: true - feedback_storage: "/tmp/data/feedback" - transcripts_enabled: true - transcripts_storage: "/tmp/data/transcripts" -authentication: - module: "noop" -inference: - default_provider: openai - default_model: gpt-4o-mini - # Unified synthesis input (Decision S5): the high-level provider entry - # replaces the default baseline's openai provider by id at synthesis time. - providers: - - type: openai - id: openai - api_key_env: OPENAI_API_KEY - allowed_models: - - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml deleted file mode 100644 index f3f9b9cdc..000000000 --- a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml +++ /dev/null @@ -1,33 +0,0 @@ -name: Lightspeed Core Service (LCS) -service: - host: 0.0.0.0 - port: 8080 - auth_enabled: false - workers: 1 - color_log: true - access_log: true -llama_stack: - # Server mode - connects to the separate llama-stack service - use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 - api_key: xyzzy -user_data_collection: - feedback_enabled: true - feedback_storage: "/tmp/data/feedback" - transcripts_enabled: true - transcripts_storage: "/tmp/data/transcripts" -authentication: - module: "noop" -inference: - default_provider: openai - default_model: gpt-4o-mini - # Unified synthesis input (Decision S5): the high-level provider entry - # replaces the default baseline's openai provider by id at synthesis time. - providers: - - type: openai - id: openai - api_key_env: OPENAI_API_KEY - allowed_models: - - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} -# INVALID: explicit legacy marker on a unified-shaped body (R11, LCORE-2872) -config_format_version: legacy diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-migrated.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-migrated.yaml new file mode 100644 index 000000000..a0555d02a --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-migrated.yaml @@ -0,0 +1,128 @@ +authentication: + module: noop +inference: + default_model: gpt-4o-mini + default_provider: openai +name: Lightspeed Core Service (LCS) +ogx: + api_key: xyzzy + config: + baseline: empty + native_override: + apis: + - responses + - batches + - files + - inference + - tool_runtime + - conversations + - vector_io + distro_name: starter + providers: + batches: + - config: + sqlstore: + backend: sql_default + table_name: batches + provider_id: reference + provider_type: inline::reference + files: + - config: + metadata_store: + backend: sql_default + table_name: files_metadata + storage_dir: ~/.llama/storage/files + provider_id: meta-reference-files + provider_type: inline::localfs + inference: + - config: + allowed_models: + - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} + api_key: ${env.OPENAI_API_KEY} + provider_id: openai + provider_type: remote::openai + - config: {} + provider_id: sentence-transformers + provider_type: inline::sentence-transformers + responses: + - config: + persistence: + responses: + backend: sql_default + table_name: agents_responses + provider_id: builtin + provider_type: inline::builtin + tool_runtime: + - config: {} + provider_id: file-search + provider_type: inline::file-search + - config: {} + provider_id: model-context-protocol + provider_type: remote::model-context-protocol + vector_io: + - config: + persistence: + backend: kv_rag + namespace: vector_io::faiss + provider_id: faiss + provider_type: inline::faiss + registered_resources: + models: + - metadata: + embedding_dimension: 768 + model_id: all-mpnet-base-v2 + model_type: embedding + provider_id: sentence-transformers + provider_model_id: all-mpnet-base-v2 + vector_stores: [] + server: + port: 8321 + storage: + backends: + kv_default: + db_path: ${env.KV_STORE_PATH:=~/.llama/storage/kv_store.db} + type: kv_sqlite + kv_rag: + db_path: ${env.KV_RAG_PATH:=~/.llama/storage/rag/kv_store.db} + type: kv_sqlite + sql_default: + db_path: ${env.SQL_STORE_PATH:=~/.llama/storage/sql_store.db} + type: sql_sqlite + stores: + connectors: + backend: sql_default + table_name: connectors + conversations: + backend: sql_default + table_name: openai_conversations + inference: + backend: sql_default + max_write_queue_size: 10000 + num_writers: 4 + table_name: inference_store + metadata: + backend: kv_default + namespace: registry + prompts: + backend: sql_default + table_name: prompts + vector_stores: + default_embedding_model: + model_id: all-mpnet-base-v2 + provider_id: sentence-transformers + default_provider_id: faiss + version: 2 + url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + use_as_library_client: false +service: + access_log: true + auth_enabled: false + color_log: true + host: 0.0.0.0 + port: 8080 + workers: 1 +user_data_collection: + feedback_enabled: true + feedback_storage: /tmp/data/feedback + transcripts_enabled: true + transcripts_storage: /tmp/data/transcripts diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-native-override-list.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-native-override-list.yaml deleted file mode 100644 index 7454bab43..000000000 --- a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-native-override-list.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: Lightspeed Core Service (LCS) -service: - host: 0.0.0.0 - port: 8080 - auth_enabled: false - workers: 1 - color_log: true - access_log: true -llama_stack: - # Server mode - connects to the separate llama-stack service - use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 - api_key: xyzzy - config: - profile: run.yaml - # R5: lists replace wholesale - the synthesized apis must equal exactly - # this list, not a merge with the baseline's (never booted - synthesis only) - native_override: - apis: - - inference - - tool_runtime -user_data_collection: - feedback_enabled: true - feedback_storage: "/tmp/data/feedback" - transcripts_enabled: true - transcripts_storage: "/tmp/data/transcripts" -authentication: - module: "noop" -inference: - default_provider: openai - default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-native-override-scalar.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-native-override-scalar.yaml deleted file mode 100644 index 3451fb526..000000000 --- a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-native-override-scalar.yaml +++ /dev/null @@ -1,30 +0,0 @@ -name: Lightspeed Core Service (LCS) -service: - host: 0.0.0.0 - port: 8080 - auth_enabled: false - workers: 1 - color_log: true - access_log: true -llama_stack: - # Server mode - connects to the separate llama-stack service - use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 - api_key: xyzzy - config: - profile: run.yaml - # R5: the raw escape hatch wins; this scalar replaces the baseline's - # safety.excluded_categories value wholesale (never booted - synthesis only) - native_override: - safety: - excluded_categories: unified-override-marker -user_data_collection: - feedback_enabled: true - feedback_storage: "/tmp/data/feedback" - transcripts_enabled: true - transcripts_storage: "/tmp/data/transcripts" -authentication: - module: "noop" -inference: - default_provider: openai - default_model: gpt-4o-mini diff --git a/tests/e2e/features/steps/unified_mode.py b/tests/e2e/features/steps/unified_mode.py index 69bd5b3f7..165e08260 100644 --- a/tests/e2e/features/steps/unified_mode.py +++ b/tests/e2e/features/steps/unified_mode.py @@ -1,465 +1,27 @@ """Step definitions for the unified-mode e2e features (LCORE-2343). -Covers configuration validation, legacy-to-unified migration, and run.yaml -synthesis for the five ``unified-mode-*.feature`` files. - -Design rules (from the LCORE-2343 planning notes): - -- Validation, migration, and synthesis steps operate on the **on-disk** - configuration artifacts — never the live service. "The active - configuration" is the repo-root ``lightspeed-stack.yaml`` copy that - ``configure_service`` applied. -- Migration and synthesis run the real CLIs as subprocesses — exactly the - surface the server entrypoint and operators use — and assertions parse - the produced YAML (data equality, never byte comparison). -- The synthesis-log step is mode-aware: in server mode the synthesis - evidence is emitted by the llama-stack container (entrypoint + CLI), not - the lightspeed-stack container the Gherkin names; the scenario's intent - (R10: the synthesized path is logged at startup) is asserted against the - container that actually synthesizes. +Only the startup-log evidence step lives here. Everything else the five +``unified-mode-*.feature`` files need — applying a configuration, restarting +containers, hitting ``readiness`` and ``query`` — resolves through the generic +steps, and the configuration validation, migration and synthesis assertions +moved to ``tests/integration/`` (``test_unified_mode_cli.py``, +``test_unified_synthesis.py``): e2e steps observe the deployed stack from +outside and never import from or execute anything under ``src/``. See +``docs/testing/e2e_testing.md``, "Choosing the Test Layer". + +The log step is mode-aware: in server mode the synthesis evidence is emitted by +the llama-stack container (entrypoint + config CLI), not the lightspeed-stack +container the Gherkin names; the scenario's intent (R10: the synthesized path +is logged at startup) is asserted against the container that actually +synthesizes. """ -import difflib -import os import re -import shutil -import stat import subprocess -import sys -import tempfile -import time -from pathlib import Path -from typing import Any, Optional -import yaml -from behave import given, step, then, when # pyright: ignore +from behave import then # pyright: ignore from behave.runner import Context -# Generated by the --migrate-config step; matches the .gitignore entry. -MIGRATED_CONFIG_BASENAME = "lightspeed-stack-unified-migrated.yaml" -# Legacy half of "the legacy migration fixture pair"; its run.yaml half is -# the repo-root run.yaml the CI harness materializes. -MIGRATION_PAIR_LCS_BASENAME = "lightspeed-stack-legacy-for-migration.yaml" - -CLI_TIMEOUT_SECONDS = 120 -CUSTOM_OUTPUT_POLL_SECONDS = 60 - - -def _mode_subdir(context: Context) -> str: - """Return the mode fixture subdirectory name for the current harness mode.""" - return "library-mode" if context.is_library_mode else "server-mode" - - -def _config_dir(context: Context) -> Path: - """Resolve the active fixture directory, mode subdir included when present. - - Mirrors ``configure_service``'s resolution so files referenced by name in - Gherkin (fixture pairs, migrated output) land where that step finds them. - """ - base = Path( - getattr(context, "lightspeed_stack_config_directory", "") - or "tests/e2e/configuration" - ) - mode_base = base / _mode_subdir(context) - return mode_base if mode_base.is_dir() else base - - -def _active_config_path() -> Path: - """Return the on-disk active configuration (the applied repo-root copy).""" - return Path("lightspeed-stack.yaml") - - -def _run_cli( - args: list[str], cwd: Optional[Path] = None -) -> subprocess.CompletedProcess: - """Run a repo CLI as a subprocess, capturing output, never raising.""" - return subprocess.run( - [sys.executable, *args], - cwd=str(cwd) if cwd else None, - capture_output=True, - text=True, - timeout=CLI_TIMEOUT_SECONDS, - check=False, - ) - - -def _load_yaml(path: Path) -> Any: - """Parse a YAML file.""" - with open(path, "r", encoding="utf-8") as f: - return yaml.safe_load(f) - - -def _native_override(config_path: Path) -> dict[str, Any]: - """Extract llama_stack.config.native_override from a config file.""" - config = _load_yaml(config_path) - override = ((config.get("llama_stack") or {}).get("config") or {}).get( - "native_override" - ) - assert override, f"{config_path} carries no llama_stack.config.native_override" - return override - - -def _synthesized(context: Context) -> Path: - """Return the synthesized run.yaml path recorded by an earlier step.""" - path = getattr(context, "synthesized_run_yaml_path", None) - assert path, "no synthesis step ran before this assertion" - return Path(path) - - -# --------------------------------------------------------------------------- -# Validation (unified-mode-validation.feature) -# --------------------------------------------------------------------------- - - -@when("configuration validation is attempted for the active configuration") -def attempt_configuration_validation(context: Context) -> None: - """Validate the on-disk active configuration via the service CLI. - - Runs ``lightspeed_stack.py --dump-configuration -c lightspeed-stack.yaml`` - as a black-box subprocess: ``main()`` loads (and thereby validates) the - configuration before any dump handling, so a Pydantic validation failure - surfaces on stderr with a non-zero exit code. cwd is the repo root so the - invalid fixtures' ``library_client_config_path: run.yaml`` resolves to the - harness-materialized run.yaml and the captured failure is the intended - cross-field error, not a file-not-found. - """ - result = _run_cli( - [ - "src/lightspeed_stack.py", - "--dump-configuration", - "-c", - str(_active_config_path()), - ] - ) - context.validation_returncode = result.returncode - context.validation_output = result.stdout + result.stderr - assert result.returncode != 0, ( - "expected the active configuration to fail validation, but the load " - f"succeeded (rc=0). Output:\n{context.validation_output}" - ) - - -@then("the validation error contains {text}") -def validation_error_contains(context: Context, text: str) -> None: - """Assert the captured validation failure mentions the given text.""" - output = getattr(context, "validation_output", None) - assert output is not None, "no validation attempt ran before this assertion" - assert ( - text.strip() in output - ), f"validation error does not contain {text!r}. Full output:\n{output}" - - -# --------------------------------------------------------------------------- -# Migration (unified-mode-migration.feature) -# --------------------------------------------------------------------------- - - -@step("lightspeed-stack --migrate-config is run for the legacy migration fixture pair") -def run_migrate_config(context: Context) -> None: - """Migrate the legacy fixture pair into the active fixture directory. - - The pair is ``lightspeed-stack-legacy-for-migration.yaml`` (mode subdir) - plus the repo-root ``run.yaml`` the harness materializes. The output lands - in the same mode subdir under the name later Gherkin steps reference, so - ``configure_service`` can boot it; it is gitignored and cleaned up after - the scenario. - """ - pair_lcs = _config_dir(context) / MIGRATION_PAIR_LCS_BASENAME - pair_run = Path("run.yaml") - output = _config_dir(context) / MIGRATED_CONFIG_BASENAME - assert pair_lcs.is_file(), f"missing migration fixture {pair_lcs}" - assert pair_run.is_file(), "repo-root run.yaml (harness-materialized) missing" - - # Migrate into a scratch directory so the CLI's own artifact keeps the - # mode it was written with and can be asserted on (R10) instead of being - # relaxed in place. - scratch = Path(tempfile.mkdtemp(prefix="lcore-e2e-migrate-")) - context.add_cleanup(lambda: shutil.rmtree(scratch, ignore_errors=True)) - cli_output = scratch / MIGRATED_CONFIG_BASENAME - - result = _run_cli( - [ - "src/lightspeed_stack.py", - "--migrate-config", - "--run-yaml", - str(pair_run), - "-c", - str(pair_lcs), - "--migrate-output", - str(cli_output), - ] - ) - assert result.returncode == 0 and cli_output.is_file(), ( - f"--migrate-config failed (rc={result.returncode}).\n" - f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" - ) - # R10: migrated files may carry lifted secrets, so the CLI must write them - # owner-only. Assert it here rather than silently relaxing the artifact. - cli_mode = stat.S_IMODE(os.stat(cli_output).st_mode) - assert cli_mode == 0o600, ( - f"--migrate-config wrote {cli_output} with mode {oct(cli_mode)}, " - "expected 0o600 (R10)" - ) - - # configure_service boots a repo-root copy of this file, and the container - # user cannot read a host-owned 0600 file. Publish a deliberate 0644 *copy* - # into the fixture directory for the harness to boot; the CLI artifact - # above keeps its 0600 mode. The fixture pair is env-reference-only by - # design, so nothing secret is widened. - shutil.copyfile(cli_output, output) - os.chmod(output, 0o644) - context.migrated_cli_output_path = cli_output - context.migrated_config_path = output - context.migration_pair_run_yaml = pair_run - context.add_cleanup(lambda: output.unlink(missing_ok=True)) - - -@then("the file {filename} contains {text}") -def file_contains(context: Context, filename: str, text: str) -> None: - """Assert a file in the active fixture directory contains a substring.""" - path = _config_dir(context) / filename.strip() - content = path.read_text(encoding="utf-8") - assert text.strip() in content, f"{path} does not contain {text!r}" - - -@then("the file {filename} does not contain {text}") -def file_does_not_contain(context: Context, filename: str, text: str) -> None: - """Assert a file in the active fixture directory lacks a substring.""" - path = _config_dir(context) / filename.strip() - content = path.read_text(encoding="utf-8") - assert text.strip() not in content, f"{path} unexpectedly contains {text!r}" - - -# --------------------------------------------------------------------------- -# Synthesis (unified-mode-synthesis.feature + migration round-trip) -# --------------------------------------------------------------------------- - - -@step("the active unified configuration is synthesized to run.yaml") -def synthesize_active_configuration(context: Context) -> None: - """Synthesize a run.yaml from the on-disk unified configuration. - - Runs the config CLI exactly as the server entrypoint does (unified - auto-detection dispatches to ``synthesize_to_file``, which also gives the - 0600 output mode). Source precedence: the migrated config when the - migration step ran in this scenario, else the active on-disk config. - When the custom-output service step ran instead, this step is a - pass-through — the service subprocess performs the synthesis. - """ - if getattr(context, "custom_output_path", None): - return - - source = getattr(context, "migrated_config_path", None) or _active_config_path() - scratch = Path(tempfile.mkdtemp(prefix="lcore-e2e-synthesis-")) - context.add_cleanup(lambda: shutil.rmtree(scratch, ignore_errors=True)) - output = scratch / "run.yaml" - - result = _run_cli( - ["src/llama_stack_configuration.py", "-c", str(source), "-o", str(output)] - ) - assert result.returncode == 0 and output.is_file(), ( - f"synthesis CLI failed (rc={result.returncode}) for {source}.\n" - f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" - ) - context.synthesized_run_yaml_path = output - - -@then( - "the synthesized run.yaml parses to the same data as the legacy migration fixture run.yaml" -) -def synthesized_round_trips(context: Context) -> None: - """Assert migrate-then-synthesize reproduces the pair's run.yaml (data equality).""" - synthesized = _load_yaml(_synthesized(context)) - original = _load_yaml( - getattr(context, "migration_pair_run_yaml", None) or Path("run.yaml") - ) - if synthesized != original: - diff = "\n".join( - difflib.unified_diff( - yaml.dump(original, sort_keys=True).splitlines(), - yaml.dump(synthesized, sort_keys=True).splitlines(), - fromfile="pair run.yaml", - tofile="synthesized", - lineterm="", - ) - ) - raise AssertionError(f"round-trip data mismatch:\n{diff}") - - -@then( - "the synthesized run.yaml contains the native_override scalar value for safety.excluded_categories" -) -def synthesized_scalar_override(context: Context) -> None: - """Assert the override's scalar replaced the baseline value at that key (R5).""" - override_value = _native_override(_active_config_path())["safety"][ - "excluded_categories" - ] - synthesized = _load_yaml(_synthesized(context)) - actual = (synthesized.get("safety") or {}).get("excluded_categories") - assert actual == override_value, ( - f"safety.excluded_categories is {actual!r}, expected the " - f"native_override value {override_value!r}" - ) - baseline = _load_yaml(Path("run.yaml")) - baseline_value = (baseline.get("safety") or {}).get("excluded_categories") - assert baseline_value != override_value, ( - "fixture and baseline agree on safety.excluded_categories — the " - "replacement assertion would be vacuous" - ) - - -@then("the synthesized run.yaml contains exactly the native_override list for apis") -def synthesized_list_override(context: Context) -> None: - """Assert the override list replaced the baseline's apis wholesale (R5).""" - override_list = _native_override(_active_config_path())["apis"] - synthesized = _load_yaml(_synthesized(context)) - assert ( - synthesized.get("apis") == override_list - ), f"apis is {synthesized.get('apis')!r}, expected exactly {override_list!r}" - baseline = _load_yaml(Path("run.yaml")) - assert ( - baseline.get("apis") != override_list - ), "fixture and baseline agree on apis — wholesale replacement would be vacuous" - - -@then("the synthesized run.yaml contains ${{env.OPENAI_API_KEY}}") -def synthesized_keeps_env_reference(context: Context) -> None: - """Assert the emitted secret stays an environment reference on disk (R6).""" - content = _synthesized(context).read_text(encoding="utf-8") - assert ( - "${env.OPENAI_API_KEY}" in content - ), "synthesized run.yaml does not carry the ${env.OPENAI_API_KEY} reference" - - -@then("the synthesized run.yaml does not contain the resolved OPENAI_API_KEY value") -def synthesized_no_literal_secret(context: Context) -> None: - """Assert the literal secret value never lands on disk (R6).""" - secret = os.environ.get("OPENAI_API_KEY", "") - assert secret, ( - "OPENAI_API_KEY is not set in the harness environment — the " - "no-literal-secret assertion would be vacuous" - ) - content = _synthesized(context).read_text(encoding="utf-8") - assert ( - secret not in content - ), "synthesized run.yaml contains the resolved OPENAI_API_KEY value" - - -@then("the synthesized run.yaml file permissions are 0600") -def synthesized_permissions(context: Context) -> None: - """Assert the synthesized file is owner-read/write only (R10).""" - mode = stat.S_IMODE(os.stat(_synthesized(context)).st_mode) - assert mode == 0o600, f"synthesized run.yaml mode is {oct(mode)}, expected 0o600" - - -# --------------------------------------------------------------------------- -# --synthesized-config-output (unified-mode-synthesis.feature) -# --------------------------------------------------------------------------- - - -@given( - "lightspeed-stack is started with --synthesized-config-output set to a custom path" -) -def start_with_custom_synthesis_output(context: Context) -> None: - """Launch a short-lived local service with a custom synthesis output path. - - The flag only affects library-mode in-process synthesis, and the running - containers cannot be restarted with different CLI args — so this step - always uses the library-mode variant of the active fixture, copied to a - scratch directory with ``service.port`` rewritten to avoid clashing with - the running stack. The subprocess synthesizes during app startup; the - following Then steps poll for the file, and the process is killed on - scenario cleanup. - """ - active_basename = _active_config_path().name - fixture_basename = Path( - getattr(context, "feature_config", "") - or "lightspeed-stack-unified-providers.yaml" - ).name - base = Path( - getattr(context, "lightspeed_stack_config_directory", "") - or "tests/e2e/configuration" - ) - library_fixture = base / "library-mode" / fixture_basename - if not library_fixture.is_file(): - library_fixture = _config_dir(context) / fixture_basename - assert library_fixture.is_file(), f"no library-mode fixture for {active_basename}" - - scratch = Path(tempfile.mkdtemp(prefix="lcore-e2e-synthout-")) - context.add_cleanup(lambda: shutil.rmtree(scratch, ignore_errors=True)) - - config = _load_yaml(library_fixture) - config.setdefault("service", {})["port"] = 8099 - scratch_config = scratch / "lightspeed-stack.yaml" - with open(scratch_config, "w", encoding="utf-8") as f: - yaml.safe_dump(config, f) - - custom_output = scratch / "custom-run.yaml" - process = subprocess.Popen( # pylint: disable=consider-using-with - [ - sys.executable, - str(Path("src/lightspeed_stack.py").resolve()), - "-c", - str(scratch_config), - "--synthesized-config-output", - str(custom_output), - ], - cwd=str(scratch), - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - ) - context.custom_output_path = custom_output - context.custom_output_scratch = scratch - context.custom_output_process = process - - def _kill() -> None: - if process.poll() is None: - process.kill() - process.wait(timeout=10) - - context.add_cleanup(_kill) - - -@then("the synthesized run.yaml is written to the custom output path") -def custom_output_written(context: Context) -> None: - """Poll for the custom-path synthesis output and validate it parses.""" - custom_output = Path(context.custom_output_path) - process = context.custom_output_process - deadline = time.monotonic() + CUSTOM_OUTPUT_POLL_SECONDS - while time.monotonic() < deadline: - if custom_output.is_file() and custom_output.stat().st_size > 0: - break - if process.poll() is not None and not custom_output.is_file(): - out = process.stdout.read() if process.stdout else "" - raise AssertionError( - f"service exited (rc={process.returncode}) before writing the " - f"custom synthesis output.\n{out[-2000:]}" - ) - time.sleep(0.5) - assert custom_output.is_file(), ( - f"custom synthesis output {custom_output} did not appear within " - f"{CUSTOM_OUTPUT_POLL_SECONDS}s" - ) - assert isinstance(_load_yaml(custom_output), dict) - context.synthesized_run_yaml_path = custom_output - - -@then("the default synthesized run.yaml path does not exist") -def default_output_absent(context: Context) -> None: - """Assert the default synthesis location was not used (override took effect).""" - scratch = Path(context.custom_output_scratch) - default_path = scratch / ".generated" / "run.yaml" - assert ( - not default_path.exists() - ), f"default synthesis path {default_path} exists despite the override" - - -# --------------------------------------------------------------------------- -# Startup logging (unified-mode-synthesis.feature) — mode-aware, see module -# docstring and LCORE-2343 planning decision Q2. -# --------------------------------------------------------------------------- - @then("the lightspeed-stack container logs contain synthesized run.yaml") def container_logs_show_synthesis(context: Context) -> None: @@ -472,10 +34,6 @@ def container_logs_show_synthesis(context: Context) -> None: scenario's intent (R10: the path is logged at startup) can only be observed on the synthesizing container. Deviation agreed in planning (Q2). - The message text follows the OGX rename (PRs #2516/#2547): client.py logs - "Using synthesized OGX config at %s" and llama_stack_configuration.py logs - "Wrote synthesized OGX configuration to %s (mode 0600)". - Every accepted pattern must carry a path. The entrypoint echoes "(mode auto-detected)" *before* synthesis runs and unconditionally, so matching it would let the scenario pass on a failed synthesis — the opposite of what diff --git a/tests/e2e/features/unified-mode-migration.feature b/tests/e2e/features/unified-mode-migration.feature index bc9565e81..8462ccc2c 100644 --- a/tests/e2e/features/unified-mode-migration.feature +++ b/tests/e2e/features/unified-mode-migration.feature @@ -8,24 +8,20 @@ Feature: Legacy to unified configuration migration And the Lightspeed stack configuration directory is "tests/e2e/configuration/unified-mode" - Scenario: migrate-config produces a unified configuration from a legacy pair - When lightspeed-stack --migrate-config is run for the legacy migration fixture pair - Then the file lightspeed-stack-unified-migrated.yaml contains native_override - And the file lightspeed-stack-unified-migrated.yaml does not contain library_client_config_path - - - Scenario: migrate then synthesize round-trips to the original run.yaml - When lightspeed-stack --migrate-config is run for the legacy migration fixture pair - And the active unified configuration is synthesized to run.yaml - Then the synthesized run.yaml parses to the same data as the legacy migration fixture run.yaml - + # The --migrate-config CLI itself (output shape, owner-only mode, migrate-then- + # synthesize round trip) is covered by tests/integration/test_unified_mode_cli.py: + # e2e steps never run src/ CLIs (docs/testing/e2e_testing.md, "Choosing the + # Test Layer"). The scenarios below boot the committed + # lightspeed-stack-unified-migrated.yaml fixture — generated once from the + # legacy migration fixture pair (lightspeed-stack-legacy-for-migration.yaml + + # tests/e2e/configs/run-ci.yaml) and guarded against CLI drift by that same + # integration module. It inlines the openai run-ci.yaml, hence @openai-only. # --- library mode (@skip-in-server-mode) --- - @skip-in-server-mode + @skip-in-server-mode @openai-only Scenario: Migrated unified configuration boots and serves queries in library mode - Given lightspeed-stack --migrate-config is run for the legacy migration fixture pair - And The service uses the lightspeed-stack-unified-migrated.yaml configuration + Given The service uses the lightspeed-stack-unified-migrated.yaml configuration And The service is restarted When I access endpoint "readiness" using HTTP GET method Then The status code of the response is 200 @@ -38,10 +34,9 @@ Feature: Legacy to unified configuration migration # --- server mode (@skip-in-library-mode) --- - @skip-in-library-mode + @skip-in-library-mode @openai-only Scenario: Migrated unified configuration boots and serves queries in server mode - Given lightspeed-stack --migrate-config is run for the legacy migration fixture pair - And The service uses the lightspeed-stack-unified-migrated.yaml configuration + Given The service uses the lightspeed-stack-unified-migrated.yaml configuration And OGX is restarted And Lightspeed Stack is restarted When I access endpoint "readiness" using HTTP GET method diff --git a/tests/e2e/features/unified-mode-synthesis.feature b/tests/e2e/features/unified-mode-synthesis.feature index f2ecf8e68..ba1409eec 100644 --- a/tests/e2e/features/unified-mode-synthesis.feature +++ b/tests/e2e/features/unified-mode-synthesis.feature @@ -7,38 +7,13 @@ Feature: Unified mode configuration synthesis And the Lightspeed stack configuration directory is "tests/e2e/configuration/unified-mode" - Scenario: native_override replaces an overlapping scalar key - Given The service uses the lightspeed-stack-unified-native-override-scalar.yaml configuration - When the active unified configuration is synthesized to run.yaml - Then the synthesized run.yaml contains the native_override scalar value for safety.excluded_categories - - - Scenario: native_override replaces an overlapping list key wholesale - Given The service uses the lightspeed-stack-unified-native-override-list.yaml configuration - When the active unified configuration is synthesized to run.yaml - Then the synthesized run.yaml contains exactly the native_override list for apis - - - Scenario: LCORE-emitted secrets remain as environment references on disk - Given The service uses the lightspeed-stack-unified-providers.yaml configuration - When the active unified configuration is synthesized to run.yaml - Then the synthesized run.yaml contains ${env.OPENAI_API_KEY} - And the synthesized run.yaml does not contain the resolved OPENAI_API_KEY value - - - Scenario: Synthesized run.yaml is written with owner-only permissions - Given The service uses the lightspeed-stack-unified-providers.yaml configuration - When the active unified configuration is synthesized to run.yaml - Then the synthesized run.yaml file permissions are 0600 - - - Scenario: synthesized-config-output overrides the default synthesis location - Given The service uses the lightspeed-stack-unified-providers.yaml configuration - And lightspeed-stack is started with --synthesized-config-output set to a custom path - When the active unified configuration is synthesized to run.yaml - Then the synthesized run.yaml is written to the custom output path - And the default synthesized run.yaml path does not exist - + # Synthesis semantics — native_override replacement (R5), secrets kept as + # environment references (R6), owner-only output mode (R10) and the + # --synthesized-config-output override — are covered in-process by + # tests/integration/test_unified_synthesis.py: e2e steps never run src/ CLIs + # (docs/testing/e2e_testing.md, "Choosing the Test Layer"). What remains here + # is the one thing only a deployed stack can show: the synthesized path is + # logged at startup (R10). # --- library mode (@skip-in-server-mode) --- diff --git a/tests/e2e/features/unified-mode-validation.feature b/tests/e2e/features/unified-mode-validation.feature deleted file mode 100644 index 9ad4dae9b..000000000 --- a/tests/e2e/features/unified-mode-validation.feature +++ /dev/null @@ -1,26 +0,0 @@ -@cfg_unified @skip-in-prow -Feature: Unified mode configuration validation - - Background: - Given The service is started locally - And The system is in default state - And the Lightspeed stack configuration directory is "tests/e2e/configuration/unified-mode" - - - Scenario: inference.providers together with library_client_config_path fails at load - Given The service uses the lightspeed-stack-invalid-providers-and-legacy.yaml configuration - When configuration validation is attempted for the active configuration - Then the validation error contains --migrate-config - - - Scenario: ogx.config together with library_client_config_path fails at load - Given The service uses the lightspeed-stack-invalid-config-and-legacy.yaml configuration - When configuration validation is attempted for the active configuration - Then the validation error contains --migrate-config - - - - Scenario: config_format_version legacy with unified-shaped body fails at load - Given The service uses the lightspeed-stack-invalid-version-legacy-unified-body.yaml configuration - When configuration validation is attempted for the active configuration - Then the validation error contains config_format_version diff --git a/tests/e2e/test_list.txt b/tests/e2e/test_list.txt index 79f34fcb5..e80a173cd 100644 --- a/tests/e2e/test_list.txt +++ b/tests/e2e/test_list.txt @@ -40,7 +40,6 @@ features/tls-tlsv13.feature features/degraded_mode_startup.feature features/unified-mode-boot.feature features/unified-mode-legacy.feature -features/unified-mode-validation.feature features/unified-mode-migration.feature features/unified-mode-synthesis.feature features/okp_rag.feature From 21bbfecbf2a50f13570f13810d77c0bcf604300e Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Fri, 4 Sep 2026 19:34:40 +0200 Subject: [PATCH 071/120] LCORE-2343: scope the startup-log evidence to the current boot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "container logs contain synthesized run.yaml" step read the container's whole log. docker logs accumulates across docker restart, and the CI baseline configurations already synthesize (library mode) or generate (server mode) on the very first compose boot, so the step could pass on a line written long before the scenario applied its unified fixture — R10 was not actually being asserted. Read only the lines since the container's current StartedAt (docker inspect), i.e. the restart the scenario just performed. In server mode the entrypoint echoes the same "Using generated config" line for the legacy-enrichment branch, so the step also requires that the fallback "Using original config:" did not appear on this boot; with a unified fixture applied, a successful generation on this boot is a synthesis. The "Wrote synthesized OGX configuration to" alternative stays accepted but is not relied on: the config CLI never configures logging, so that INFO line is dropped in the llama-stack container. Also collapse the LCS restart to a single wait path. restart_container already waits for Docker health and then for HTTP on the published port (the harness fix earlier in this branch), which made the explicit wait_for_lightspeed_stack_http_ready calls in the proxy and TLS steps redundant and turned restart_lightspeed_stack_service's wait_http flag into a no-op with a docstring that said the opposite. Remove both; the degraded-mode caller drops the dead argument. The integration legacy-pair fixture named tests/configuration/run.yaml as its library_client_config_path while the migrate test passes tests/e2e/configs/run-ci.yaml as --run-yaml; point the fixture at the pair that is actually migrated. --- ...lightspeed-stack-legacy-for-migration.yaml | 2 +- tests/e2e/features/steps/common.py | 2 +- tests/e2e/features/steps/proxy.py | 2 - tests/e2e/features/steps/tls.py | 2 - tests/e2e/features/steps/unified_mode.py | 52 +++++++++++++++---- tests/e2e/utils/utils.py | 14 ++--- 6 files changed, 49 insertions(+), 25 deletions(-) diff --git a/tests/configuration/unified-mode/lightspeed-stack-legacy-for-migration.yaml b/tests/configuration/unified-mode/lightspeed-stack-legacy-for-migration.yaml index bf4bfe96c..396b24fcb 100644 --- a/tests/configuration/unified-mode/lightspeed-stack-legacy-for-migration.yaml +++ b/tests/configuration/unified-mode/lightspeed-stack-legacy-for-migration.yaml @@ -10,7 +10,7 @@ llama_stack: # Library mode - embeds the stack in-process use_as_library_client: true # Legacy two-file shape: external run.yaml, no synthesis input - library_client_config_path: tests/configuration/run.yaml + library_client_config_path: tests/e2e/configs/run-ci.yaml user_data_collection: feedback_enabled: true feedback_storage: "/tmp/data/feedback" diff --git a/tests/e2e/features/steps/common.py b/tests/e2e/features/steps/common.py index ba49db388..5ce825486 100644 --- a/tests/e2e/features/steps/common.py +++ b/tests/e2e/features/steps/common.py @@ -213,7 +213,7 @@ def restart_service_without_restoring_ogx(context: Context) -> None: if getattr(context, "lightspeed_stack_skip_restart", False): context.lightspeed_stack_skip_restart = False return - restart_lightspeed_stack_service(skip_ogx_restore=True, wait_http=False) + restart_lightspeed_stack_service(skip_ogx_restore=True) @given("The system is in default state") diff --git a/tests/e2e/features/steps/proxy.py b/tests/e2e/features/steps/proxy.py index ddb8d4c50..f5651b6cb 100644 --- a/tests/e2e/features/steps/proxy.py +++ b/tests/e2e/features/steps/proxy.py @@ -42,7 +42,6 @@ from tests.e2e.utils.utils import ( is_prow_environment, restart_container, - wait_for_lightspeed_stack_http_ready, ) _CLUSTER_INTERCEPTION_PROXY_PORTS = frozenset( @@ -339,7 +338,6 @@ def restart_ogx(context: Context) -> None: def restart_lightspeed_stack(context: Context) -> None: """Restart the Lightspeed Stack container.""" restart_container("lightspeed-stack") - wait_for_lightspeed_stack_http_ready() # --- Tunnel Proxy Steps --- diff --git a/tests/e2e/features/steps/tls.py b/tests/e2e/features/steps/tls.py index a87715d2d..443704184 100644 --- a/tests/e2e/features/steps/tls.py +++ b/tests/e2e/features/steps/tls.py @@ -98,7 +98,6 @@ def _restart_lightspeed_after_ogx_tls(context: Context) -> None: """ from tests.e2e.utils.utils import ( restart_container, - wait_for_lightspeed_stack_http_ready, ) scenario = getattr(getattr(context, "scenario", None), "name", "") or "?" @@ -111,7 +110,6 @@ def _restart_lightspeed_after_ogx_tls(context: Context) -> None: flush=True, ) restart_container("lightspeed-stack") - wait_for_lightspeed_stack_http_ready() def restart_ogx_for_tls_feature(context: Context) -> None: diff --git a/tests/e2e/features/steps/unified_mode.py b/tests/e2e/features/steps/unified_mode.py index 165e08260..23e0e3af5 100644 --- a/tests/e2e/features/steps/unified_mode.py +++ b/tests/e2e/features/steps/unified_mode.py @@ -23,21 +23,49 @@ from behave.runner import Context +def _container_started_at(container: str) -> str: + """Return the container's last start timestamp (RFC 3339) from ``docker inspect``.""" + result = subprocess.run( + ["docker", "inspect", "-f", "{{.State.StartedAt}}", container], + capture_output=True, + text=True, + timeout=60, + check=False, + ) + assert ( + result.returncode == 0 + ), f"docker inspect {container} failed: {result.stderr[-500:]}" + started_at = result.stdout.strip() + assert started_at, f"docker inspect {container} returned no StartedAt" + return started_at + + @then("the lightspeed-stack container logs contain synthesized run.yaml") def container_logs_show_synthesis(context: Context) -> None: """Assert the container that synthesizes logged the synthesized-config path. Library mode: the lightspeed-stack container itself synthesizes in-process and logs "Using synthesized OGX config at ". Server mode: synthesis - happens in the llama-stack container (entrypoint + config CLI), which logs - the generated-config path — the Gherkin names lightspeed-stack, but the - scenario's intent (R10: the path is logged at startup) can only be observed - on the synthesizing container. Deviation agreed in planning (Q2). + happens in the llama-stack container (entrypoint + config CLI), which + echoes the generated-config path — the Gherkin names lightspeed-stack, but + the scenario's intent (R10: the path is logged at startup) can only be + observed on the synthesizing container. Deviation agreed in planning (Q2). + + ``docker logs`` accumulates across ``docker restart``, and the CI baseline + configurations already synthesize (library) or generate (server) on the + very first compose boot — so an unscoped read would pass on evidence from + an earlier boot. The read is therefore limited to lines since the + container's current ``StartedAt``, i.e. the restart the scenario just + performed under the unified fixture. Every accepted pattern must carry a path. The entrypoint echoes "(mode - auto-detected)" *before* synthesis runs and unconditionally, so matching it - would let the scenario pass on a failed synthesis — the opposite of what - R10 asks. "Using generated config: " is only echoed on success. + auto-detected)" *before* generation runs and unconditionally, so matching + it would let the scenario pass on a failed synthesis — the opposite of + what R10 asks. "Using generated config: " is echoed only on + success; it is the same line for the legacy-enrichment branch, so in + server mode the step additionally requires that the fallback line + "Using original config:" did not appear on this boot. The applied fixture + is unified, so a successful generation on this boot is a synthesis. """ if context.is_library_mode: container = "lightspeed-stack" @@ -49,8 +77,9 @@ def container_logs_show_synthesis(context: Context) -> None: r"|Using generated config:\s*\S+" ) + started_at = _container_started_at(container) result = subprocess.run( - ["docker", "logs", container], + ["docker", "logs", "--since", started_at, container], capture_output=True, text=True, timeout=60, @@ -61,6 +90,11 @@ def container_logs_show_synthesis(context: Context) -> None: ), f"docker logs {container} failed: {result.stderr[-500:]}" logs = result.stdout + result.stderr assert re.search(pattern, logs), ( - f"{container} logs carry no synthesis-path evidence " + f"{container} logs since {started_at} carry no synthesis-path evidence " f"(pattern {pattern!r} not found)" ) + if not context.is_library_mode: + assert "Using original config:" not in logs, ( + f"{container} fell back to the original run.yaml on this boot; " + "the unified fixture was not synthesized" + ) diff --git a/tests/e2e/utils/utils.py b/tests/e2e/utils/utils.py index 82b132ec9..0bab7f97c 100644 --- a/tests/e2e/utils/utils.py +++ b/tests/e2e/utils/utils.py @@ -564,19 +564,15 @@ def restart_container(container_name: str) -> None: reset_ogx_disrupt_once_tracking() -def restart_lightspeed_stack_service( - *, wait_http: bool = False, skip_ogx_restore: bool = False -) -> None: +def restart_lightspeed_stack_service(*, skip_ogx_restore: bool = False) -> None: """Restart the lightspeed-stack container used by Behave steps. - Wraps ``restart_container("lightspeed-stack")`` and optionally polls the - host-mapped port so step modules share one LCS restart path. + Wraps ``restart_container("lightspeed-stack")`` so step modules share one + LCS restart path. That path already waits for Docker health and then for + HTTP on the host-mapped port, so callers need no wait of their own. Parameters: ---------- - wait_http: When True, also call ``wait_for_lightspeed_stack_http_ready`` - after Docker health. Default False — generic ``The service is - restarted`` relies on Docker health only; proxy/tls steps opt in. skip_ogx_restore: When True on Prow/Konflux, tell e2e-ops not to bring llama back before recreating LCS (degraded-mode startup). """ @@ -585,8 +581,6 @@ def restart_lightspeed_stack_service( os.environ["E2E_SKIP_OGX_RESTORE_ON_LCS_RESTART"] = "1" try: restart_container("lightspeed-stack") - if wait_http: - wait_for_lightspeed_stack_http_ready() finally: if skip_ogx_restore: if previous is None: From af990c2f904bc62f66e8e812662099ba05ca3c18 Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Fri, 4 Sep 2026 19:38:03 +0200 Subject: [PATCH 072/120] LCORE-2343: keep the fixture regeneration recipe within the line limit The drift-guard docstring spelled out both fixture paths on single lines, which pylint rejects at 100 columns. Factor the mode directory into a variable in the recipe. --- tests/integration/test_unified_mode_cli.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/integration/test_unified_mode_cli.py b/tests/integration/test_unified_mode_cli.py index 09a2b735e..3987af8dc 100644 --- a/tests/integration/test_unified_mode_cli.py +++ b/tests/integration/test_unified_mode_cli.py @@ -158,13 +158,14 @@ def test_committed_migrated_fixture_matches_cli_output( ``tests/e2e/configuration/unified-mode//lightspeed-stack-unified-migrated.yaml`` instead of generating it in a step (e2e steps never run ``src/`` CLIs). This guard fails the moment ``--migrate-config`` output drifts from the - committed file. To refresh the fixture, run from the repo root:: + committed file. To refresh the fixture, run from the repo root with + ``DIR=tests/e2e/configuration/unified-mode/``:: uv run python src/lightspeed_stack.py --migrate-config \\ --run-yaml tests/e2e/configs/run-ci.yaml \\ - -c tests/e2e/configuration/unified-mode//lightspeed-stack-legacy-for-migration.yaml \\ - --migrate-output tests/e2e/configuration/unified-mode//lightspeed-stack-unified-migrated.yaml - chmod 644 tests/e2e/configuration/unified-mode//lightspeed-stack-unified-migrated.yaml + -c $DIR/lightspeed-stack-legacy-for-migration.yaml \\ + --migrate-output $DIR/lightspeed-stack-unified-migrated.yaml + chmod 644 $DIR/lightspeed-stack-unified-migrated.yaml """ output = tmp_path / "migrated.yaml" _migrate(_E2E_FIXTURES / mode / _LEGACY_PAIR_FIXTURE, _E2E_RUN_YAML, output) From adc041b72f270177c5f52e4bd04f427e4256e2bc Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Thu, 10 Sep 2026 04:26:28 +0200 Subject: [PATCH 073/120] LCORE-2343: drop a stray blank line from .gitignore The rework added a trailing blank line with no accompanying entry. Pure diff noise against a file this PR otherwise has no business touching. Co-Authored-By: Claude Opus 5 --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0d5d1e1c1..3d7a1f08f 100644 --- a/.gitignore +++ b/.gitignore @@ -202,4 +202,3 @@ local-run.yaml .sisyphus/ # Per-developer feature design overrides (see docs/contributing/feature-design.config) .feature-design.config.local - From 30e53ad95175fe8897139236bb36e14d1ff8b11d Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Thu, 10 Sep 2026 04:26:28 +0200 Subject: [PATCH 074/120] LCORE-2343: tag the synthesis scenarios @openai-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both scenarios apply lightspeed-stack-unified-providers.yaml, which tests/e2e/configuration/unified-mode/README.md describes as "openai-specific — used by @openai-only scenarios", and which every other scenario that applies it already tags: unified-mode-boot.feature:13 and :65, unified-mode-migration.feature:20 and :37. Untagged, the two scenarios run on the azure/vertexai/watsonx/bedrock matrix (e2e_tests_providers.yaml) and on vllm (e2e_tests_rhaiis.yaml), where they replace the deployed stack with an openai-only synthesized run.yaml — in server mode also regenerating the llama-stack container's config. They pass there today only because OPENAI_API_KEY happens to be set in every matrix; the first matrix without it would leave the container unable to start. Co-Authored-By: Claude Opus 5 --- tests/e2e/features/unified-mode-synthesis.feature | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/features/unified-mode-synthesis.feature b/tests/e2e/features/unified-mode-synthesis.feature index ba1409eec..528a4683a 100644 --- a/tests/e2e/features/unified-mode-synthesis.feature +++ b/tests/e2e/features/unified-mode-synthesis.feature @@ -17,7 +17,7 @@ Feature: Unified mode configuration synthesis # --- library mode (@skip-in-server-mode) --- - @skip-in-server-mode + @skip-in-server-mode @openai-only Scenario: Synthesized run.yaml path is logged at startup in library mode Given The service uses the lightspeed-stack-unified-providers.yaml configuration And The service is restarted @@ -26,7 +26,7 @@ Feature: Unified mode configuration synthesis # --- server mode (@skip-in-library-mode) --- - @skip-in-library-mode + @skip-in-library-mode @openai-only Scenario: Synthesized run.yaml path is logged at startup in server mode Given The service uses the lightspeed-stack-unified-providers.yaml configuration And OGX is restarted From ea3333bd60129e09195c287d8224c9fbb8b67b42 Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Thu, 10 Sep 2026 04:26:45 +0200 Subject: [PATCH 075/120] LCORE-2343: keep the after_feature restore tolerant, and bound the wait honestly Wiring wait_for_lightspeed_stack_http_ready into every lightspeed-stack restart turned restart_container from a soft failure into a hard one for that container. Inside a scenario that is what we want. In after_feature it is not: environment.py restores the config backup and restarts under E2E_RESTORE_CONFIG_AFTER_FEATURE=1, and an AssertionError raised from a behave hook is a hook error that aborts the whole run rather than failing a single scenario. The teardown restart now warns and continues; the next feature's own restart surfaces a genuinely dead service. restart_container's docstring gains the Raises entry it was missing, including the note that Docker health stays a soft failure while the HTTP wait does not. Two corrections to the rewritten wait itself: - attempt was incremented before the deadline check, so a wait that expired reported one more attempt than it actually made. - the docstring claimed the total wait cannot exceed timeout_s. requests applies its scalar timeout to the connect and the read phase separately, so an attempt started just under the deadline can overrun by up to request_timeout_s. The claim is now stated with that bound. Co-Authored-By: Claude Opus 5 --- tests/e2e/features/environment.py | 10 +++++++++- tests/e2e/utils/utils.py | 14 +++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/e2e/features/environment.py b/tests/e2e/features/environment.py index 02112d40f..95d349ffe 100644 --- a/tests/e2e/features/environment.py +++ b/tests/e2e/features/environment.py @@ -545,7 +545,15 @@ def after_feature(context: Context, feature: Feature) -> None: remove_config_backup(backup_path) if not context.is_library_mode: restart_container("ogx") - restart_container("lightspeed-stack") + # restart_container hard-fails for lightspeed-stack when the + # service does not accept HTTP in time. That is right inside a + # scenario, but this runs in after_feature: an exception here is a + # hook error that takes down the whole run rather than failing one + # scenario. Warn and let the next feature's own restart surface it. + try: + restart_container("lightspeed-stack") + except AssertionError as exc: + print(f"⚠ after_feature restore: lightspeed-stack not ready ({exc})") reset_active_lightspeed_stack_config_basename() else: remove_config_backup(backup_path) diff --git a/tests/e2e/utils/utils.py b/tests/e2e/utils/utils.py index 0bab7f97c..c10e87d49 100644 --- a/tests/e2e/utils/utils.py +++ b/tests/e2e/utils/utils.py @@ -519,6 +519,11 @@ def restart_container(container_name: str) -> None: Raises: subprocess.CalledProcessError: if the `docker restart` command fails. subprocess.TimeoutExpired: if the `docker restart` command times out. + AssertionError: for ``lightspeed-stack``, if the service does not + accept HTTP within ``wait_for_lightspeed_stack_http_ready``'s + budget. Docker health itself stays a soft failure; the HTTP wait + does not, so callers that must not fail (teardown hooks) have to + guard the call. """ if is_prow_environment(): restart_pod(container_name) @@ -605,8 +610,11 @@ def wait_for_lightspeed_stack_http_ready( Bounded by a single monotonic deadline covering both the requests and the sleeps, and each request is additionally capped at the time remaining, so - the total wait cannot exceed ``timeout_s``. An attempt-counted loop cannot - give that guarantee: with a per-request timeout the worst case is + the wait stays within ``timeout_s`` plus at most one request timeout — + ``requests`` applies its scalar ``timeout`` to the connect and the read + phase separately, so an attempt started just under the deadline can + overrun by that much. An attempt-counted loop cannot give even that + guarantee: with a per-request timeout the worst case is ``attempts * request_timeout + (attempts - 1) * delay``, which for the previous defaults was 518.5s while the failure message reported 120s. @@ -628,10 +636,10 @@ def wait_for_lightspeed_stack_http_ready( deadline = started + timeout_s attempt = 0 while True: - attempt += 1 remaining = deadline - time.monotonic() if remaining <= 0: break + attempt += 1 try: response = requests.get(url, timeout=min(request_timeout_s, remaining)) if response.status_code in (200, 401): From d1742b067bc595997207b50c3c4bf4b7f5c24219 Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Thu, 10 Sep 2026 04:27:04 +0200 Subject: [PATCH 076/120] LCORE-2343: make server-mode synthesis observable, and assert on it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server-mode synthesis scenario could not fail for the reason it claimed. It accepted either "Wrote synthesized OGX configuration to " or "Using generated config: ". The first never appears: ogx_configuration.py runs as a bare script from scripts/ogx-entrypoint.sh, nothing installs a handler on the root logger there, and logging.lastResort emits WARNING and above only, so that logger.info line is dropped before it reaches the container log. The second appears for both branches of the entrypoint, whose own comment reads "Generate config (synthesis or enrichment)". The compensating guard did not discriminate either: "Using original config:" is printed only when generation failed, so it is absent from a successful enrichment too. The scenario therefore passed whenever generation succeeded, whatever was generated. Had the unified fixture not reached the container — config copy skipped, wrong mount, fixture reverted to a legacy shape — the entrypoint would have enriched a run.yaml, exited 0, printed the same line, and the scenario would have gone green with R10 unverified. main() in src/ogx_configuration.py now calls setup_logging() before doing any work, so the INFO lines this module writes reach the container log. That is the fix the black-box rule needs: the reason an e2e test reaches into src/ is that the deployed stack emits no evidence, and the answer is to make it emit evidence rather than to reach inside. AsyncOgxClient already does exactly this before synthesis (src/client/ogx.py), with a comment describing the same failure on the in-process path; the CLI path never got the same treatment. Independent of the tests this is an observability bug in its own right: the config CLI has been silent at INFO in every container boot since it was written, so the entrypoint's 2>&1 captured nothing about which config shape was detected or where output was written. With that line reaching the log, the server-mode pattern narrows to it alone, and both modes now match a line only the synthesis path writes. The step's docstring records the reasoning; its previous version argued that a successful generation on this boot must be a synthesis because the applied fixture is unified, which assumes the very thing the scenario exists to prove. The fallback guard also moves above the pattern assert. When the entrypoint genuinely falls back, the pattern is absent too, so the generic "no synthesis-path evidence" message fired first and the purpose-built "fell back to the original run.yaml on this boot" message was unreachable in exactly the case it was written for. Co-Authored-By: Claude Opus 5 --- src/ogx_configuration.py | 15 ++++++++- tests/e2e/features/steps/unified_mode.py | 39 ++++++++++++++---------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/ogx_configuration.py b/src/ogx_configuration.py index d5092bc41..bf6b06457 100644 --- a/src/ogx_configuration.py +++ b/src/ogx_configuration.py @@ -30,7 +30,7 @@ from pydantic import SecretStr import constants -from log import get_logger +from log import get_logger, setup_logging logger = get_logger(__name__) @@ -1550,6 +1550,10 @@ def main() -> None: run.yaml needs to exist; otherwise the legacy path enriches the ``--input`` run.yaml in place. Server-mode container entrypoints rely on this dispatch to serve both modes with a single invocation. + + Configures logging first so the INFO lines this module emits reach the + container log: run as a bare script there is no handler on the root + logger, and ``logging.lastResort`` would drop everything below WARNING. """ parser = ArgumentParser( description="Generate the OGX run configuration from a " @@ -1577,6 +1581,15 @@ def main() -> None: ) args = parser.parse_args() + # Configure logging before doing any work. This module runs as a bare + # script from the container entrypoint (scripts/ogx-entrypoint.sh), so + # nothing has installed a handler on the root logger; Python's lastResort + # then emits WARNING and above only, and every INFO line this module + # writes -- including which config shape was detected and where the + # synthesized run.yaml was written -- is silently dropped. AsyncOgxClient + # already does this for the in-process path, for the same reason. + setup_logging() + with open(args.config, encoding="utf-8") as f: config = yaml.safe_load(f) diff --git a/tests/e2e/features/steps/unified_mode.py b/tests/e2e/features/steps/unified_mode.py index 23e0e3af5..a83eefc56 100644 --- a/tests/e2e/features/steps/unified_mode.py +++ b/tests/e2e/features/steps/unified_mode.py @@ -58,24 +58,29 @@ def container_logs_show_synthesis(context: Context) -> None: container's current ``StartedAt``, i.e. the restart the scenario just performed under the unified fixture. - Every accepted pattern must carry a path. The entrypoint echoes "(mode - auto-detected)" *before* generation runs and unconditionally, so matching - it would let the scenario pass on a failed synthesis — the opposite of - what R10 asks. "Using generated config: " is echoed only on - success; it is the same line for the legacy-enrichment branch, so in - server mode the step additionally requires that the fallback line - "Using original config:" did not appear on this boot. The applied fixture - is unified, so a successful generation on this boot is a synthesis. + The pattern must carry a path and must be unique to synthesis. The + entrypoint's own lines cannot provide that: it echoes "(mode + auto-detected)" before generation runs and unconditionally, and it echoes + "Using generated config: " identically for the synthesis and the + legacy-enrichment branch (scripts/ogx-entrypoint.sh). Matching either + would let the scenario pass when the unified fixture never reached the + container and the entrypoint enriched a run.yaml instead — which is the + one thing this scenario exists to rule out. The fallback line "Using + original config:" does not discriminate either; it is printed only when + generation *failed*, so it is absent from a successful enrichment too. + + So both modes match a line that only the synthesis path writes: + src/client/ogx.py in library mode, src/ogx_configuration.py in server + mode. The latter reaches the container log only because main() configures + logging — as a bare script nothing installs a root handler and + logging.lastResort drops everything below WARNING. """ if context.is_library_mode: container = "lightspeed-stack" pattern = r"Using synthesized OGX config at \S+" else: container = "llama-stack" - pattern = ( - r"Wrote synthesized OGX configuration to \S+" - r"|Using generated config:\s*\S+" - ) + pattern = r"Wrote synthesized OGX configuration to \S+" started_at = _container_started_at(container) result = subprocess.run( @@ -89,12 +94,14 @@ def container_logs_show_synthesis(context: Context) -> None: result.returncode == 0 ), f"docker logs {container} failed: {result.stderr[-500:]}" logs = result.stdout + result.stderr - assert re.search(pattern, logs), ( - f"{container} logs since {started_at} carry no synthesis-path evidence " - f"(pattern {pattern!r} not found)" - ) + # Checked before the pattern assert: when the entrypoint genuinely fell + # back, the pattern is absent too, and this is the message that says why. if not context.is_library_mode: assert "Using original config:" not in logs, ( f"{container} fell back to the original run.yaml on this boot; " "the unified fixture was not synthesized" ) + assert re.search(pattern, logs), ( + f"{container} logs since {started_at} carry no synthesis-path evidence " + f"(pattern {pattern!r} not found)" + ) From 57e5410f7087636579fd9db70fdb079b462610d4 Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Fri, 11 Sep 2026 15:07:49 +0200 Subject: [PATCH 077/120] LCORE-2343: follow the OGX runtime rename in the unified-mode fixtures and step #2605 (a50b004c, "finish OGX runtime naming cleanup") renamed the server-mode compose service and container from llama-stack to ogx, and the host variable the lightspeed-stack container receives from E2E_LLAMA_HOSTNAME to E2E_OGX_HOSTNAME. Rebasing onto it resolved the textual conflicts, but two uses on this branch did not conflict and would have failed at run time: - The six server-mode unified-mode fixtures built the OGX url from ${env.E2E_LLAMA_HOSTNAME}, which is no longer set inside the container, so server-mode boots would get an unresolvable url. They now use ${env.E2E_OGX_HOSTNAME}, as every server-mode fixture on main does. The committed migrated fixture and its legacy input change together, so the --migrate-config drift guard still matches. - The synthesis step read `docker logs llama-stack` in server mode. The container is now ogx. The comments next to those lines follow the same rename. The llama_stack: keys in these fixtures stay as they are: that alias is still accepted. Checked with behave --dry-run (133 steps across the four unified-mode features, none undefined) and the unified-mode integration tests (26 passed, including the drift guard). --- .../server-mode/lightspeed-stack-legacy-for-migration.yaml | 4 ++-- .../lightspeed-stack-unified-absolute-profile.yaml | 6 +++--- .../server-mode/lightspeed-stack-unified-config-only.yaml | 4 ++-- .../server-mode/lightspeed-stack-unified-migrated.yaml | 2 +- .../server-mode/lightspeed-stack-unified-providers.yaml | 4 ++-- .../lightspeed-stack-unified-relative-profile.yaml | 4 ++-- tests/e2e/features/steps/unified_mode.py | 6 +++--- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-legacy-for-migration.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-legacy-for-migration.yaml index 76b2ac36d..18890c9fc 100644 --- a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-legacy-for-migration.yaml +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-legacy-for-migration.yaml @@ -7,9 +7,9 @@ service: color_log: true access_log: true llama_stack: - # Server mode - connects to the separate llama-stack service + # Server mode - connects to the separate ogx service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-absolute-profile.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-absolute-profile.yaml index 1a1c4d8a1..7f9b31dd5 100644 --- a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-absolute-profile.yaml +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-absolute-profile.yaml @@ -7,12 +7,12 @@ service: color_log: true access_log: true llama_stack: - # Server mode - connects to the separate llama-stack service + # Server mode - connects to the separate ogx service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy config: - # Absolute path as mounted in the llama-stack container + # Absolute path as mounted in the ogx container profile: /opt/app-root/run.yaml user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-config-only.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-config-only.yaml index 3881fdf4e..34ace93fa 100644 --- a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-config-only.yaml +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-config-only.yaml @@ -7,9 +7,9 @@ service: color_log: true access_log: true llama_stack: - # Server mode - connects to the separate llama-stack service + # Server mode - connects to the separate ogx service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy config: # Synthesis baseline: the CI-materialized run.yaml (provider-agnostic) diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-migrated.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-migrated.yaml index a0555d02a..7a31fc7af 100644 --- a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-migrated.yaml +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-migrated.yaml @@ -112,7 +112,7 @@ ogx: provider_id: sentence-transformers default_provider_id: faiss version: 2 - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 use_as_library_client: false service: access_log: true diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-providers.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-providers.yaml index 4ca585947..a40465cca 100644 --- a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-providers.yaml +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-providers.yaml @@ -7,9 +7,9 @@ service: color_log: true access_log: true llama_stack: - # Server mode - connects to the separate llama-stack service + # Server mode - connects to the separate ogx service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-relative-profile.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-relative-profile.yaml index 3c6c8512a..0647f4c87 100644 --- a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-relative-profile.yaml +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-relative-profile.yaml @@ -7,9 +7,9 @@ service: color_log: true access_log: true llama_stack: - # Server mode - connects to the separate llama-stack service + # Server mode - connects to the separate ogx service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy config: # R8: relative profile resolves against this file's loaded location diff --git a/tests/e2e/features/steps/unified_mode.py b/tests/e2e/features/steps/unified_mode.py index a83eefc56..3e110ed2c 100644 --- a/tests/e2e/features/steps/unified_mode.py +++ b/tests/e2e/features/steps/unified_mode.py @@ -10,7 +10,7 @@ ``docs/testing/e2e_testing.md``, "Choosing the Test Layer". The log step is mode-aware: in server mode the synthesis evidence is emitted by -the llama-stack container (entrypoint + config CLI), not the lightspeed-stack +the ogx container (entrypoint + config CLI), not the lightspeed-stack container the Gherkin names; the scenario's intent (R10: the synthesized path is logged at startup) is asserted against the container that actually synthesizes. @@ -46,7 +46,7 @@ def container_logs_show_synthesis(context: Context) -> None: Library mode: the lightspeed-stack container itself synthesizes in-process and logs "Using synthesized OGX config at ". Server mode: synthesis - happens in the llama-stack container (entrypoint + config CLI), which + happens in the ogx container (entrypoint + config CLI), which echoes the generated-config path — the Gherkin names lightspeed-stack, but the scenario's intent (R10: the path is logged at startup) can only be observed on the synthesizing container. Deviation agreed in planning (Q2). @@ -79,7 +79,7 @@ def container_logs_show_synthesis(context: Context) -> None: container = "lightspeed-stack" pattern = r"Using synthesized OGX config at \S+" else: - container = "llama-stack" + container = "ogx" pattern = r"Wrote synthesized OGX configuration to \S+" started_at = _container_started_at(container) From ea1cd68844889a67c86158e8a552d04ad4191455 Mon Sep 17 00:00:00 2001 From: Anik Bhattacharjee Date: Fri, 11 Sep 2026 10:47:28 -0400 Subject: [PATCH 078/120] LCORE-2119: Test custom OpenTelemetry spans across endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verify that request handlers emit their custom OpenTelemetry spans with the expected names, attributes, and events, splitting coverage by test level: individual spans are asserted at the unit level and cross-component span hierarchy at the integration level. Unit level (individual spans — presence, attributes, events, incl. negative paths): - Add test_query_otel.py: query.handle_request root attributes/events on success, plus a quota-exceeded (429) case where attributes are recorded but lifecycle events are absent. - Add a responses LLM-failure case: validation.completed is recorded but llm.response.completed is not when the model call raises. Integration level (parent-child hierarchy only, where components interact): - Add test_feedback_otel_trace.py: feedback.storage nests under feedback.submit (shared trace, correct parentage). - query and responses span trees remain covered by the existing test_otel_trace_propagation.py and test_responses_otel_trace.py. Signed-off-by: Anik Bhattacharjee --- tests/integration/conftest.py | 3 + tests/integration/test_feedback_otel_trace.py | 95 ++++++++ tests/unit/app/endpoints/test_query_otel.py | 206 ++++++++++++++++++ .../unit/app/endpoints/test_responses_otel.py | 48 ++++ 4 files changed, 352 insertions(+) create mode 100644 tests/integration/test_feedback_otel_trace.py create mode 100644 tests/unit/app/endpoints/test_query_otel.py diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 55e3ead36..7ea307159 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -544,8 +544,11 @@ def set_streaming_query_agent_run( OTEL_INSTRUMENTED_MODULES = ( + "app.endpoints.authorized", + "app.endpoints.feedback", "app.endpoints.query", "app.endpoints.responses", + "utils.agents.query", "utils.quota_utils", "utils.responses", "utils.shields", diff --git a/tests/integration/test_feedback_otel_trace.py b/tests/integration/test_feedback_otel_trace.py new file mode 100644 index 000000000..bac9b569e --- /dev/null +++ b/tests/integration/test_feedback_otel_trace.py @@ -0,0 +1,95 @@ +"""Integration tests for OpenTelemetry span tree on POST /feedback. + +This module covers the component interaction the feedback handler produces: +the ``feedback.storage`` span is opened inside the ``feedback.submit`` span, +so the two must share a trace and ``feedback.storage`` must be parented to +``feedback.submit``. +""" + +from pathlib import Path + +import pytest +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) +from pytest_mock import MockerFixture + +from app.endpoints.feedback import feedback_endpoint_handler +from authentication.interface import AuthTuple +from configuration import configuration +from models.api.requests import FeedbackRequest +from models.common.feedback import FeedbackCategory + +ROOT_SPAN_NAME = "feedback.submit" +STORAGE_SPAN_NAME = "feedback.storage" +FEEDBACK_CONVERSATION_ID = "12345678-abcd-0000-0123-456789abcdef" + + +@pytest.fixture(autouse=True) +def _clear_spans(otel_collector: InMemorySpanExporter) -> None: + """Clear collected spans before each test.""" + otel_collector.clear() + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("test_config") +async def test_feedback_storage_span_nested_under_submit( + tmp_path: Path, + test_auth: AuthTuple, + otel_collector: InMemorySpanExporter, + mocker: MockerFixture, +) -> None: + """POST /feedback nests feedback.storage under feedback.submit. + + Feedback storage is pointed at a writable temp directory so the real + filesystem write drives a successful storage span. The test asserts only the + parent-child hierarchy: both spans exist, share a single trace, and + ``feedback.storage`` is parented to ``feedback.submit``. Individual span + attributes and events are covered at the unit level. + + Args: + tmp_path: Writable temp directory used as the feedback storage location. + test_auth: Authentication tuple from the real noop auth dependency. + otel_collector: In-memory OTEL exporter collecting finished spans. + mocker: pytest-mock fixture used to patch conversation retrieval. + """ + user_id, _, _, _ = test_auth + configuration.user_data_collection_configuration.feedback_storage = str(tmp_path) + + # The conversation must exist and belong to the authenticated user. + mock_conversation = mocker.Mock() + mock_conversation.user_id = user_id + mocker.patch( + "app.endpoints.feedback.retrieve_conversation", + return_value=mock_conversation, + ) + + result = await feedback_endpoint_handler( + feedback_request=FeedbackRequest( + conversation_id=FEEDBACK_CONVERSATION_ID, + user_question="What is Kubernetes?", + llm_response="Kubernetes is an open-source container orchestrator.", + user_feedback="The answer was too vague.", + sentiment=-1, + categories=[FeedbackCategory.INCORRECT, FeedbackCategory.INCOMPLETE], + ), + auth=test_auth, + _ensure_feedback_enabled=None, + ) + assert result.response == "feedback received" + + spans = otel_collector.get_finished_spans() + span_names = {span.name for span in spans} + missing = {ROOT_SPAN_NAME, STORAGE_SPAN_NAME} - span_names + assert not missing, f"Missing expected spans: {missing}" + + root = next(span for span in spans if span.name == ROOT_SPAN_NAME) + storage = next(span for span in spans if span.name == STORAGE_SPAN_NAME) + assert root.context is not None + assert storage.context is not None + + assert root.context.trace_id == storage.context.trace_id + assert storage.parent is not None, "feedback.storage should have a parent" + assert ( + storage.parent.span_id == root.context.span_id + ), "feedback.storage should be parented to feedback.submit" diff --git a/tests/unit/app/endpoints/test_query_otel.py b/tests/unit/app/endpoints/test_query_otel.py new file mode 100644 index 000000000..9b76e6f14 --- /dev/null +++ b/tests/unit/app/endpoints/test_query_otel.py @@ -0,0 +1,206 @@ +# pylint: disable=redefined-outer-name +"""OpenTelemetry unit tests for the /query REST API endpoint.""" + +from typing import Any + +import pytest +from fastapi import HTTPException, Request, status +from ogx_client import AsyncOgxClient +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) +from pytest_mock import MockerFixture + +from app.endpoints.query import query_endpoint_handler +from configuration import AppConfig +from models.api.requests import QueryRequest +from models.api.responses.error import QuotaExceededResponse +from models.common.moderation import ShieldModerationPassed +from models.common.responses.responses_api_params import ResponsesApiParams +from models.common.turn_summary import TurnSummary +from quota.quota_exceed_error import QuotaExceedError +from utils.otel_tracing import SpanAttributes, SpanEvents + +MODULE = "app.endpoints.query" +QUERY_SPAN_NAME = "query.handle_request" +QUERY_TEXT = "What is Kubernetes?" + +# User ID must be a proper UUID. +MOCK_AUTH = ( + "00000001-0001-0001-0001-000000000001", + "mock_username", + False, + "mock_token", +) + + +@pytest.fixture(name="dummy_request") +def dummy_request_fixture() -> Request: + """Minimal FastAPI Request for query endpoint unit tests.""" + return Request(scope={"type": "http", "headers": []}) + + +@pytest.fixture(name="minimal_config") +def minimal_config_fixture() -> AppConfig: + """Minimal AppConfig for query endpoint OTEL tests.""" + cfg = AppConfig() + cfg.init_from_dict( + { + "name": "test", + "service": {"host": "localhost", "port": 8080}, + "ogx": { + "api_key": "test-key", + "url": "http://test.com:1234", + "use_as_library_client": False, + }, + "user_data_collection": {"transcripts_enabled": False}, + "mcp_servers": [], + "conversation_cache": {"type": "noop"}, + } + ) + return cfg + + +def _patch_query_success(mocker: MockerFixture) -> None: + """Patch the query handler dependencies for a successful turn.""" + mocker.patch(f"{MODULE}.check_configuration_loaded") + mocker.patch(f"{MODULE}.check_tokens_available") + mocker.patch(f"{MODULE}.validate_model_provider_override") + mocker.patch(f"{MODULE}.check_mcp_auth", new=mocker.AsyncMock()) + + mock_response_obj = mocker.Mock() + mock_response_obj.output = [] + mock_client = mocker.AsyncMock(spec=AsyncOgxClient) + mock_client.responses = mocker.Mock() + mock_client.responses.create = mocker.AsyncMock(return_value=mock_response_obj) + mock_holder = mocker.Mock() + mock_holder.get_client.return_value = mock_client + mocker.patch(f"{MODULE}.AsyncOgxClientHolder", return_value=mock_holder) + + mocker.patch( + f"{MODULE}.maybe_get_topic_summary", + new=mocker.AsyncMock(return_value=None), + ) + mocker.patch( + f"{MODULE}.run_shield_moderation", + new=mocker.AsyncMock(return_value=ShieldModerationPassed()), + ) + + mock_params = mocker.Mock(spec=ResponsesApiParams) + mock_params.model = "provider1/model1" + mock_params.conversation = "conv_123" + mock_params.tools = None + mock_params.model_dump.return_value = {"input": "test", "model": "provider1/model1"} + mocker.patch( + f"{MODULE}.prepare_responses_params", + new=mocker.AsyncMock(return_value=mock_params), + ) + + turn_summary = TurnSummary() + turn_summary.llm_response = "Kubernetes is a container orchestration platform" + mocker.patch( + f"{MODULE}.retrieve_agent_response", + new=mocker.AsyncMock(return_value=turn_summary), + ) + + mocker.patch(f"{MODULE}.normalize_conversation_id", return_value="123") + mocker.patch(f"{MODULE}.store_query_results") + mocker.patch(f"{MODULE}.consume_query_tokens") + mocker.patch(f"{MODULE}.get_available_quotas", return_value={}) + + +@pytest.mark.asyncio +async def test_query_root_span_attributes_and_events( + dummy_request: Request, + minimal_config: AppConfig, + mocker: MockerFixture, + otel: tuple[Any, InMemorySpanExporter], +) -> None: + """The /query root span carries setup attributes and all lifecycle events. + + The mocked success path validates the request, persists the turn, and + completes the LLM response, so the validation/turn-persisted/LLM-response + events are all recorded, and the anonymized user/input attributes are set. + """ + tracer, exporter = otel + mocker.patch(f"{MODULE}.configuration", minimal_config) + mocker.patch(f"{MODULE}.tracer", tracer) + mocker.patch( + f"{MODULE}.anonymize_value", side_effect=lambda value: f"[anon:{value}]" + ) + _patch_query_success(mocker) + + await query_endpoint_handler( + request=dummy_request, + query_request=QueryRequest( + query=QUERY_TEXT + ), # pyright: ignore[reportCallIssue] + auth=MOCK_AUTH, + mcp_headers={}, + ) + + root = next(s for s in exporter.get_finished_spans() if s.name == QUERY_SPAN_NAME) + attrs = dict(root.attributes or {}) + assert attrs[SpanAttributes.USER_ID] == f"[anon:{MOCK_AUTH[0]}]" + assert attrs[SpanAttributes.INPUT] == f"[anon:{QUERY_TEXT}]" + assert attrs[SpanAttributes.REQUEST_ATTACHMENTS_COUNT] == 0 + assert SpanAttributes.OUTPUT in attrs + assert SpanAttributes.SESSION_ID in attrs + + event_names = {event.name for event in root.events} + assert SpanEvents.VALIDATION_COMPLETED in event_names + assert SpanEvents.TURN_PERSISTED in event_names + assert SpanEvents.LLM_RESPONSE_COMPLETED in event_names + + +@pytest.mark.asyncio +async def test_query_quota_exceeded_records_attributes_without_events( + dummy_request: Request, + minimal_config: AppConfig, + mocker: MockerFixture, + otel: tuple[Any, InMemorySpanExporter], +) -> None: + """A 429 from the quota check leaves the root span attributes but no events. + + The quota check runs after the root attributes are recorded but before any + lifecycle event fires. When it raises HTTP 429 the request is aborted, so the + ``query.handle_request`` span is still exported with its user/input + attributes, but none of the validation/turn-persisted/LLM-response events are + recorded, and tracing does not crash on the error path. + """ + tracer, exporter = otel + mocker.patch(f"{MODULE}.configuration", minimal_config) + mocker.patch(f"{MODULE}.tracer", tracer) + mocker.patch( + f"{MODULE}.anonymize_value", side_effect=lambda value: f"[anon:{value}]" + ) + mocker.patch(f"{MODULE}.check_configuration_loaded") + mocker.patch(f"{MODULE}.check_mcp_auth", new=mocker.AsyncMock()) + + def _raise_quota_exceeded(*_args: object, **_kwargs: object) -> None: + error = QuotaExceedError(subject_id=MOCK_AUTH[0], subject_type="u", available=0) + raise HTTPException(**QuotaExceededResponse.from_exception(error).model_dump()) + + mocker.patch(f"{MODULE}.check_tokens_available", side_effect=_raise_quota_exceeded) + + with pytest.raises(HTTPException) as exc_info: + await query_endpoint_handler( + request=dummy_request, + query_request=QueryRequest( # pyright: ignore[reportCallIssue] + query=QUERY_TEXT + ), + auth=MOCK_AUTH, + mcp_headers={}, + ) + assert exc_info.value.status_code == status.HTTP_429_TOO_MANY_REQUESTS + + root = next(s for s in exporter.get_finished_spans() if s.name == QUERY_SPAN_NAME) + attrs = dict(root.attributes or {}) + assert attrs[SpanAttributes.USER_ID] == f"[anon:{MOCK_AUTH[0]}]" + assert attrs[SpanAttributes.INPUT] == f"[anon:{QUERY_TEXT}]" + assert attrs[SpanAttributes.REQUEST_ATTACHMENTS_COUNT] == 0 + + event_names = {event.name for event in root.events} + assert SpanEvents.VALIDATION_COMPLETED not in event_names + assert SpanEvents.TURN_PERSISTED not in event_names + assert SpanEvents.LLM_RESPONSE_COMPLETED not in event_names diff --git a/tests/unit/app/endpoints/test_responses_otel.py b/tests/unit/app/endpoints/test_responses_otel.py index b1de65e81..e6139a71e 100644 --- a/tests/unit/app/endpoints/test_responses_otel.py +++ b/tests/unit/app/endpoints/test_responses_otel.py @@ -25,11 +25,15 @@ from models.config import Action from tests.unit.app.endpoints.responses_otel_helpers import ( MOCK_AUTH, + MODEL, MODULE, + OTEL_CONV_ID, + ROOT_SPAN_NAME, assert_root_setup_attributes, find_span, make_turn_summary_with_tools, make_turn_summary_without_tools, + patch_handler_success_mocks, patch_responses_endpoint_setup, patch_responses_otel_tracers, run_responses_setup_smoke, @@ -252,3 +256,47 @@ async def test_streaming_root_span_closed_on_setup_error( ) find_span(exporter.get_finished_spans(), "responses.handle_request") + + @pytest.mark.asyncio + async def test_llm_failure_keeps_validation_without_response_event( + self, + dummy_request: Request, + minimal_config: AppConfig, + mocker: MockerFixture, + otel: tuple[Any, InMemorySpanExporter], + ) -> None: + """A failing LLM call keeps validation.completed but drops llm.response.completed. + + ``validation.completed`` fires before the model is called; the + LLM-response event only fires once the turn is finalized. Forcing + ``responses.create`` to raise aborts the request after validation, so the + root span is exported with the validation event but without the + LLM-response event, and tracing does not crash on the error path. + """ + tracer, exporter = otel + patch_responses_otel_tracers(mocker, tracer, minimal_config) + mock_client = patch_responses_endpoint_setup(mocker, minimal_config) + patch_handler_success_mocks(mocker) + mock_client.responses.create = mocker.AsyncMock( + side_effect=ApiException(status=None, reason="connection failed") + ) + + with pytest.raises(HTTPException): + await responses_endpoint_handler( + request=dummy_request, + responses_request=ResponsesRequest( + input=INPUT_TEXT, + model=MODEL, + stream=False, + store=False, + conversation=OTEL_CONV_ID, + generate_topic_summary=False, + ), + auth=MOCK_AUTH, + mcp_headers={}, + ) + + root = find_span(exporter.get_finished_spans(), ROOT_SPAN_NAME) + event_names = [event.name for event in root.events] + assert SpanEvents.VALIDATION_COMPLETED in event_names + assert SpanEvents.LLM_RESPONSE_COMPLETED not in event_names From 5b39b128c4142f2d294ada4b948c960e1b6953f3 Mon Sep 17 00:00:00 2001 From: "red-hat-konflux-kflux-prd-rh02[bot]" <190377777+red-hat-konflux-kflux-prd-rh02[bot]@users.noreply.github.com> Date: Sat, 12 Sep 2026 08:05:38 +0000 Subject: [PATCH 079/120] Update Konflux references Signed-off-by: red-hat-konflux-kflux-prd-rh02 <190377777+red-hat-konflux-kflux-prd-rh02[bot]@users.noreply.github.com> --- .../lightspeed-stack-0-8-pull-request.yaml | 26 +++++++++---------- .tekton/lightspeed-stack-0-8-push.yaml | 26 +++++++++---------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.tekton/lightspeed-stack-0-8-pull-request.yaml b/.tekton/lightspeed-stack-0-8-pull-request.yaml index aca9e80b0..df5012a37 100644 --- a/.tekton/lightspeed-stack-0-8-pull-request.yaml +++ b/.tekton/lightspeed-stack-0-8-pull-request.yaml @@ -193,7 +193,7 @@ spec: - name: name value: init - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-init:0.4.3@sha256:4be9343579d91c7b501cafe966cff59a601dfeca903c476e3763dd8d7599b900 + value: quay.io/konflux-ci/tekton-catalog/task-init:0.4.3@sha256:c6c414a3b5dcd827720f3443a128a02d27a0a5071ea3e4dc0063d578f1f73a74 - name: kind value: task resolver: bundles @@ -214,7 +214,7 @@ spec: - name: name value: git-clone-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-git-clone-oci-ta:0.2.6@sha256:2e8fe30b6d5c8a8a3e6bbc0ea5a55e05b6170d4a399830f25ea43e17881ce544 + value: quay.io/konflux-ci/tekton-catalog/task-git-clone-oci-ta:0.2.6@sha256:1d7ba568ae6e9e26800054f3942779de8abc80ddc48f0e44df6b1c2f098161dc - name: kind value: task resolver: bundles @@ -240,7 +240,7 @@ spec: - name: name value: prefetch-dependencies-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies-oci-ta:0.10.2@sha256:374f776bcb2048c3adeaf4dbb460c52d001bc6020320df5e878c2d05391302da + value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies-oci-ta:0.10.3@sha256:9fd7d251f92250d89a5465756e278e053af28c6356e5ccdedc4be87bbb13c190 - name: kind value: task resolver: bundles @@ -304,7 +304,7 @@ spec: - name: name value: buildah-remote-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-buildah-remote-oci-ta:0.12.1@sha256:ceecb10bc58092c51a104f62ce6fd188a2d3f06fbdc446d81801cab118929344 + value: quay.io/konflux-ci/tekton-catalog/task-buildah-remote-oci-ta:0.12.1@sha256:9afb0e30b1b5b49a5a934a09ce3b55baf892da99a784387ff72ec92bd2c374c2 - name: kind value: task resolver: bundles @@ -326,7 +326,7 @@ spec: - name: name value: build-image-index - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.3.1@sha256:290c9ec319423ff9ae7b2cb78fa859e1d333abcdd2ef6c001533377812020071 + value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.3.1@sha256:6ead03ca82a1b970dc60eaadae86adf374e8e70f8d951902e1fac7411dbe0550 - name: kind value: task resolver: bundles @@ -347,7 +347,7 @@ spec: - name: name value: source-build-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-source-build-oci-ta:0.3.1@sha256:1808485d95cf77fb7912f6fe69191bead05fc0f2f71e00031941a7ea38a5f665 + value: quay.io/konflux-ci/tekton-catalog/task-source-build-oci-ta:0.3.1@sha256:bbad5bfd67d1263320a0c4d3f062a18ae611e081b009fd96117c2a00a6ce8dd9 - name: kind value: task resolver: bundles @@ -421,7 +421,7 @@ spec: - name: name value: ecosystem-cert-preflight-checks - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:c07d2befa8abf48d4a223a5bdf5ea2335e756d4d0bbc422761087c263060aa94 + value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:fa53ef450ba538addafee1211ff6ac15b55865ce652fec15ac99b554680cb642 - name: kind value: task resolver: bundles @@ -451,7 +451,7 @@ spec: - name: name value: sast-snyk-check-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check-oci-ta:0.5@sha256:67a409de3c99aeaee4596da3081f26955ca6201a7f12cb6a9912659bdbcc4d01 + value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check-oci-ta:0.5@sha256:a973b32f0e11958aba08a635981e4d4e7eb200c417492371a1e692ff589b4e8f - name: kind value: task resolver: bundles @@ -574,7 +574,7 @@ spec: - name: name value: sast-shell-check-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-sast-shell-check-oci-ta:0.1@sha256:afa8ba8859739e48b672f66fa2af357d27f4d96846a7c0ad84e38f21b043f695 + value: quay.io/konflux-ci/tekton-catalog/task-sast-shell-check-oci-ta:0.1@sha256:d9b01530ce3c20287714e64980f154e28c0e94d17e2dbfbaf3c8bf77b1844b9e - name: kind value: task resolver: bundles @@ -602,7 +602,7 @@ spec: - name: name value: sast-unicode-check-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-sast-unicode-check-oci-ta:0.4@sha256:69d5fca2fb94dcc7df32e36e4828e6fb24b9ba55b1837c331a83e20d8dfd479e + value: quay.io/konflux-ci/tekton-catalog/task-sast-unicode-check-oci-ta:0.4@sha256:381750451fbcc86d90fa83579a48e0e6555d7cf374fe2c4af3f9262a17fc3c89 - name: kind value: task resolver: bundles @@ -624,7 +624,7 @@ spec: - name: name value: apply-tags - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.3.1@sha256:ccd3665345d86c6799bc7e2e6ad86b277d9f3a5c40b513e6f2a0af8ad92e7dba + value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.3.1@sha256:b1c71d83be8e6d0e44f9b6e58e1ad491396d4c287be9291802c9be69d9441f2f - name: kind value: task resolver: bundles @@ -647,7 +647,7 @@ spec: - name: name value: push-dockerfile-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile-oci-ta:0.3.1@sha256:ef00a86cb22259fcfdefa15a5116b63d0f24ee35c95d05ff9815ee8f84beb548 + value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile-oci-ta:0.3.1@sha256:3e59d6303ca031c27fab079f7c743fbdecc53d007b8d86e32227de5164f06e31 - name: kind value: task resolver: bundles @@ -664,7 +664,7 @@ spec: - name: name value: rpms-signature-scan - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2.2@sha256:9ef4dabd53e823e3139b99c8de708be4ee759d63b32982847929df19ab75b2f8 + value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2.2@sha256:64bb1cf7d875157e68043a411f00e7852afffc68f50881fb041ba994109f4763 - name: kind value: task resolver: bundles diff --git a/.tekton/lightspeed-stack-0-8-push.yaml b/.tekton/lightspeed-stack-0-8-push.yaml index 938ba86a1..5848afae6 100644 --- a/.tekton/lightspeed-stack-0-8-push.yaml +++ b/.tekton/lightspeed-stack-0-8-push.yaml @@ -194,7 +194,7 @@ spec: - name: name value: init - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-init:0.4.3@sha256:4be9343579d91c7b501cafe966cff59a601dfeca903c476e3763dd8d7599b900 + value: quay.io/konflux-ci/tekton-catalog/task-init:0.4.3@sha256:c6c414a3b5dcd827720f3443a128a02d27a0a5071ea3e4dc0063d578f1f73a74 - name: kind value: task resolver: bundles @@ -215,7 +215,7 @@ spec: - name: name value: git-clone-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-git-clone-oci-ta:0.2.6@sha256:2e8fe30b6d5c8a8a3e6bbc0ea5a55e05b6170d4a399830f25ea43e17881ce544 + value: quay.io/konflux-ci/tekton-catalog/task-git-clone-oci-ta:0.2.6@sha256:1d7ba568ae6e9e26800054f3942779de8abc80ddc48f0e44df6b1c2f098161dc - name: kind value: task resolver: bundles @@ -241,7 +241,7 @@ spec: - name: name value: prefetch-dependencies-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies-oci-ta:0.10.2@sha256:374f776bcb2048c3adeaf4dbb460c52d001bc6020320df5e878c2d05391302da + value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies-oci-ta:0.10.3@sha256:9fd7d251f92250d89a5465756e278e053af28c6356e5ccdedc4be87bbb13c190 - name: kind value: task resolver: bundles @@ -305,7 +305,7 @@ spec: - name: name value: buildah-remote-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-buildah-remote-oci-ta:0.12.1@sha256:ceecb10bc58092c51a104f62ce6fd188a2d3f06fbdc446d81801cab118929344 + value: quay.io/konflux-ci/tekton-catalog/task-buildah-remote-oci-ta:0.12.1@sha256:9afb0e30b1b5b49a5a934a09ce3b55baf892da99a784387ff72ec92bd2c374c2 - name: kind value: task resolver: bundles @@ -327,7 +327,7 @@ spec: - name: name value: build-image-index - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.3.1@sha256:290c9ec319423ff9ae7b2cb78fa859e1d333abcdd2ef6c001533377812020071 + value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.3.1@sha256:6ead03ca82a1b970dc60eaadae86adf374e8e70f8d951902e1fac7411dbe0550 - name: kind value: task resolver: bundles @@ -348,7 +348,7 @@ spec: - name: name value: source-build-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-source-build-oci-ta:0.3.1@sha256:1808485d95cf77fb7912f6fe69191bead05fc0f2f71e00031941a7ea38a5f665 + value: quay.io/konflux-ci/tekton-catalog/task-source-build-oci-ta:0.3.1@sha256:bbad5bfd67d1263320a0c4d3f062a18ae611e081b009fd96117c2a00a6ce8dd9 - name: kind value: task resolver: bundles @@ -422,7 +422,7 @@ spec: - name: name value: ecosystem-cert-preflight-checks - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:c07d2befa8abf48d4a223a5bdf5ea2335e756d4d0bbc422761087c263060aa94 + value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:fa53ef450ba538addafee1211ff6ac15b55865ce652fec15ac99b554680cb642 - name: kind value: task resolver: bundles @@ -452,7 +452,7 @@ spec: - name: name value: sast-snyk-check-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check-oci-ta:0.5@sha256:67a409de3c99aeaee4596da3081f26955ca6201a7f12cb6a9912659bdbcc4d01 + value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check-oci-ta:0.5@sha256:a973b32f0e11958aba08a635981e4d4e7eb200c417492371a1e692ff589b4e8f - name: kind value: task resolver: bundles @@ -575,7 +575,7 @@ spec: - name: name value: sast-shell-check-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-sast-shell-check-oci-ta:0.1@sha256:afa8ba8859739e48b672f66fa2af357d27f4d96846a7c0ad84e38f21b043f695 + value: quay.io/konflux-ci/tekton-catalog/task-sast-shell-check-oci-ta:0.1@sha256:d9b01530ce3c20287714e64980f154e28c0e94d17e2dbfbaf3c8bf77b1844b9e - name: kind value: task resolver: bundles @@ -603,7 +603,7 @@ spec: - name: name value: sast-unicode-check-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-sast-unicode-check-oci-ta:0.4@sha256:69d5fca2fb94dcc7df32e36e4828e6fb24b9ba55b1837c331a83e20d8dfd479e + value: quay.io/konflux-ci/tekton-catalog/task-sast-unicode-check-oci-ta:0.4@sha256:381750451fbcc86d90fa83579a48e0e6555d7cf374fe2c4af3f9262a17fc3c89 - name: kind value: task resolver: bundles @@ -628,7 +628,7 @@ spec: - name: name value: apply-tags - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.3.1@sha256:ccd3665345d86c6799bc7e2e6ad86b277d9f3a5c40b513e6f2a0af8ad92e7dba + value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.3.1@sha256:b1c71d83be8e6d0e44f9b6e58e1ad491396d4c287be9291802c9be69d9441f2f - name: kind value: task resolver: bundles @@ -651,7 +651,7 @@ spec: - name: name value: push-dockerfile-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile-oci-ta:0.3.1@sha256:ef00a86cb22259fcfdefa15a5116b63d0f24ee35c95d05ff9815ee8f84beb548 + value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile-oci-ta:0.3.1@sha256:3e59d6303ca031c27fab079f7c743fbdecc53d007b8d86e32227de5164f06e31 - name: kind value: task resolver: bundles @@ -668,7 +668,7 @@ spec: - name: name value: rpms-signature-scan - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2.2@sha256:9ef4dabd53e823e3139b99c8de708be4ee759d63b32982847929df19ab75b2f8 + value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2.2@sha256:64bb1cf7d875157e68043a411f00e7852afffc68f50881fb041ba994109f4763 - name: kind value: task resolver: bundles From 2065a81913460b13180f0a141e1291129b9bc48c Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Sun, 13 Sep 2026 09:32:27 +0200 Subject: [PATCH 080/120] LCORE-2493: Benchmarks for tokenizing Go code --- tests/benchmarks/data/go_10000_lines.go | 10000 +++++++++++++++++++++ tests/benchmarks/data/go_1000_lines.go | 1000 +++ tests/benchmarks/data/go_100_lines.go | 100 + tests/benchmarks/data/go_10_lines.go | 10 + tests/benchmarks/test_token_estimator.py | 56 + 5 files changed, 11166 insertions(+) create mode 100644 tests/benchmarks/data/go_10000_lines.go create mode 100644 tests/benchmarks/data/go_1000_lines.go create mode 100644 tests/benchmarks/data/go_100_lines.go create mode 100644 tests/benchmarks/data/go_10_lines.go diff --git a/tests/benchmarks/data/go_10000_lines.go b/tests/benchmarks/data/go_10000_lines.go new file mode 100644 index 000000000..e9e1b4a15 --- /dev/null +++ b/tests/benchmarks/data/go_10000_lines.go @@ -0,0 +1,10000 @@ +// +// Apache License +// Version 2.0, January 2004 +// http://www.apache.org/licenses/ +// +// TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +// +// 1. Definitions. +// +// "License" shall mean the terms and conditions for use, reproduction, +// and distribution as defined by Sections 1 through 9 of this document. +// +// "Licensor" shall mean the copyright owner or entity authorized by +// the copyright owner that is granting the License. +// +// "Legal Entity" shall mean the union of the acting entity and all +// other entities that control, are controlled by, or are under common +// control with that entity. For the purposes of this definition, +// "control" means (i) the power, direct or indirect, to cause the +// direction or management of such entity, whether by contract or +// otherwise, or (ii) ownership of fifty percent (50%) or more of the +// outstanding shares, or (iii) beneficial ownership of such entity. +// +// "You" (or "Your") shall mean an individual or Legal Entity +// exercising permissions granted by this License. +// +// "Source" form shall mean the preferred form for making modifications, +// including but not limited to software source code, documentation +// source, and configuration files. +// +// "Object" form shall mean any form resulting from mechanical +// transformation or translation of a Source form, including but +// not limited to compiled object code, generated documentation, +// and conversions to other media types. +// +// "Work" shall mean the work of authorship, whether in Source or +// Object form, made available under the License, as indicated by a +// copyright notice that is included in or attached to the work +// (an example is provided in the Appendix below). +// +// "Derivative Works" shall mean any work, whether in Source or Object +// form, that is based on (or derived from) the Work and for which the +// editorial revisions, annotations, elaborations, or other modifications +// represent, as a whole, an original work of authorship. For the purposes +// of this License, Derivative Works shall not include works that remain +// separable from, or merely link (or bind by name) to the interfaces of, +// the Work and Derivative Works thereof. +// +// "Contribution" shall mean any work of authorship, including +// the original version of the Work and any modifications or additions +// to that Work or Derivative Works thereof, that is intentionally +// submitted to Licensor for inclusion in the Work by the copyright owner +// or by an individual or Legal Entity authorized to submit on behalf of +// the copyright owner. For the purposes of this definition, "submitted" +// means any form of electronic, verbal, or written communication sent +// to the Licensor or its representatives, including but not limited to +// communication on electronic mailing lists, source code control systems, +// and issue tracking systems that are managed by, or on behalf of, the +// Licensor for the purpose of discussing and improving the Work, but +// excluding communication that is conspicuously marked or otherwise +// designated in writing by the copyright owner as "Not a Contribution." +// +// "Contributor" shall mean Licensor and any individual or Legal Entity +// on behalf of whom a Contribution has been received by Licensor and +// subsequently incorporated within the Work. +// +// 2. Grant of Copyright License. Subject to the terms and conditions of +// this License, each Contributor hereby grants to You a perpetual, +// worldwide, non-exclusive, no-charge, royalty-free, irrevocable +// copyright license to reproduce, prepare Derivative Works of, +// publicly display, publicly perform, sublicense, and distribute the +// Work and such Derivative Works in Source or Object form. +// +// 3. Grant of Patent License. Subject to the terms and conditions of +// this License, each Contributor hereby grants to You a perpetual, +// worldwide, non-exclusive, no-charge, royalty-free, irrevocable +// (except as stated in this section) patent license to make, have made, +// use, offer to sell, sell, import, and otherwise transfer the Work, +// where such license applies only to those patent claims licensable +// by such Contributor that are necessarily infringed by their +// Contribution(s) alone or by combination of their Contribution(s) +// with the Work to which such Contribution(s) was submitted. If You +// institute patent litigation against any entity (including a +// cross-claim or counterclaim in a lawsuit) alleging that the Work +// or a Contribution incorporated within the Work constitutes direct +// or contributory patent infringement, then any patent licenses +// granted to You under this License for that Work shall terminate +// as of the date such litigation is filed. +// +// 4. Redistribution. You may reproduce and distribute copies of the +// Work or Derivative Works thereof in any medium, with or without +// modifications, and in Source or Object form, provided that You +// meet the following conditions: +// +// (a) You must give any other recipients of the Work or +// Derivative Works a copy of this License; and +// +// (b) You must cause any modified files to carry prominent notices +// stating that You changed the files; and +// +// (c) You must retain, in the Source form of any Derivative Works +// that You distribute, all copyright, patent, trademark, and +// attribution notices from the Source form of the Work, +// excluding those notices that do not pertain to any part of +// the Derivative Works; and +// +// (d) If the Work includes a "NOTICE" text file as part of its +// distribution, then any Derivative Works that You distribute must +// include a readable copy of the attribution notices contained +// within such NOTICE file, excluding those notices that do not +// pertain to any part of the Derivative Works, in at least one +// of the following places: within a NOTICE text file distributed +// as part of the Derivative Works; within the Source form or +// documentation, if provided along with the Derivative Works; or, +// within a display generated by the Derivative Works, if and +// wherever such third-party notices normally appear. The contents +// of the NOTICE file are for informational purposes only and +// do not modify the License. You may add Your own attribution +// notices within Derivative Works that You distribute, alongside +// or as an addendum to the NOTICE text from the Work, provided +// that such additional attribution notices cannot be construed +// as modifying the License. +// +// You may add Your own copyright statement to Your modifications and +// may provide additional or different license terms and conditions +// for use, reproduction, or distribution of Your modifications, or +// for any such Derivative Works as a whole, provided Your use, +// reproduction, and distribution of the Work otherwise complies with +// the conditions stated in this License. +// +// 5. Submission of Contributions. Unless You explicitly state otherwise, +// any Contribution intentionally submitted for inclusion in the Work +// by You to the Licensor shall be under the terms and conditions of +// this License, without any additional terms or conditions. +// Notwithstanding the above, nothing herein shall supersede or modify +// the terms of any separate license agreement you may have executed +// with Licensor regarding such Contributions. +// +// 6. Trademarks. This License does not grant permission to use the trade +// names, trademarks, service marks, or product names of the Licensor, +// except as required for reasonable and customary use in describing the +// origin of the Work and reproducing the content of the NOTICE file. +// +// 7. Disclaimer of Warranty. Unless required by applicable law or +// agreed to in writing, Licensor provides the Work (and each +// Contributor provides its Contributions) on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied, including, without limitation, any warranties or conditions +// of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +// PARTICULAR PURPOSE. You are solely responsible for determining the +// appropriateness of using or redistributing the Work and assume any +// risks associated with Your exercise of permissions under this License. +// +// 8. Limitation of Liability. In no event and under no legal theory, +// whether in tort (including negligence), contract, or otherwise, +// unless required by applicable law (such as deliberate and grossly +// negligent acts) or agreed to in writing, shall any Contributor be +// liable to You for damages, including any direct, indirect, special, +// incidental, or consequential damages of any character arising as a +// result of this License or out of the use or inability to use the +// Work (including but not limited to damages for loss of goodwill, +// work stoppage, computer failure or malfunction, or any and all +// other commercial damages or losses), even if such Contributor +// has been advised of the possibility of such damages. +// +// 9. Accepting Warranty or Additional Liability. While redistributing +// the Work or Derivative Works thereof, You may choose to offer, +// and charge a fee for, acceptance of support, warranty, indemnity, +// or other liability obligations and/or rights consistent with this +// License. However, in accepting such obligations, You may act only +// on Your own behalf and on Your sole responsibility, not on behalf +// of any other Contributor, and only if You agree to indemnify, +// defend, and hold each Contributor harmless for any liability +// incurred by, or claims asserted against, such Contributor by reason +// of your accepting any such warranty or additional liability. +// +// END OF TERMS AND CONDITIONS +// +// APPENDIX: How to apply the Apache License to your work. +// +// To apply the Apache License to your work, attach the following +// boilerplate notice, with the fields enclosed by brackets "[]" +// replaced with your own identifying information. (Don't include +// the brackets!) The text should be enclosed in the appropriate +// comment syntax for the file format. We also recommend that a +// file or class name and description of purpose be included on the +// same "printed page" as the copyright notice for easier +// identification within third-party archives. +// +// Copyright [yyyy] [name of copyright owner] +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// + +package main + +import ( + "errors" + "fmt" + "image" + "log" + "math" + "math/cmplx" + "os" +) + +// IImage is representation of raster image consisting of IPixels +type IImage [][]IPixel + +// NewIImage constructs new instance of ZImage +func NewIImage(resolution Resolution) IImage { + iimage := make([][]IPixel, resolution.Height) + for y := uint(0); y < resolution.Height; y++ { + iimage[y] = make([]IPixel, resolution.Width) + } + return iimage +} + +// IPixel is a representation of pixel as one unsigned integer value +type IPixel uint64 + +// RImage is representation of raster image consisting of RPixels +type RImage [][]RPixel + +// NewRImage constructs new instance of RImage +func NewRImage(resolution Resolution) RImage { + rimage := make([][]RPixel, resolution.Height) + for y := uint(0); y < resolution.Height; y++ { + rimage[y] = make([]RPixel, resolution.Width) + } + return rimage +} + +func (image *RImage) minMax(width, height uint) (float64, float64) { + min := float64(math.Inf(1)) + max := float64(math.Inf(-1)) + + for j := range height { + for i := range width { + z := float64((*image)[j][i]) + if max < z { + max = z + } + if min > z { + min = z + } + } + } + return min, max +} + +// RPixel is a representation of pixel as one real value +type RPixel float64 + +// ZImage is representation of raster image consisting of ZPixels +type ZImage [][]ZPixel + +// NewZImage constructs new instance of ZImage +func NewZImage(resolution Resolution) ZImage { + zimage := make([][]ZPixel, resolution.Height) + for y := uint(0); y < resolution.Height; y++ { + zimage[y] = make([]ZPixel, resolution.Width) + } + return zimage +} + +// ZPixel is a representation of pixel in complex plane +type ZPixel complex128 + +// Palette structure +type Palette struct { + Name string `toml:"name"` + Shift int `toml:"shift"` + Slope int `toml:"slope"` +} + +// FractalParameter structure contains information about all fractal parameters. +type FractalParameter struct { + Name string `toml:"name"` + Type string `toml:"type"` + Class string `toml:"class"` + Cx0 float64 `toml:"cx0"` + Cy0 float64 `toml:"cy0"` + Palette Palette `toml:"palette"` + Maxiter uint `toml:"maxiter"` + Bailout uint `toml:"bailout"` + Function1 string `toml:"function1"` + Function2 string `toml:"function2"` + Xmin float64 `toml:"xmin"` + Ymin float64 `toml:"ymin"` + Xmax float64 `toml:"xmax"` + Ymax float64 `toml:"ymax"` + A float64 `toml:"A"` + B float64 `toml:"B"` + C float64 `toml:"C"` + D float64 `toml:"D"` + Scale float64 `toml:"scale"` + XOffset float64 `toml:"x_offset"` + YOffset float64 `toml:"y_offset"` +} + +// FractalParameter2 structure contains information about all fractal parameters. +type FractalParameter2 struct { + Name string `toml:"name"` + Type string `toml:"type"` + Class string `toml:"class"` + Cx0 float64 `toml:"cx0"` + Cy0 float64 `toml:"cy0"` + Palette Palette `toml:"palette"` + Maxiter uint `toml:"maxiter"` + Bailout uint `toml:"bailout"` + Function1 string `toml:"function1"` + Function2 string `toml:"function2"` + Xmin float64 `toml:"xmin"` + Ymin float64 `toml:"ymin"` + Xmax float64 `toml:"xmax"` + Ymax float64 `toml:"ymax"` + A float64 `toml:"A"` + B float64 `toml:"B"` + C float64 `toml:"C"` + D float64 `toml:"D"` + Scale float64 `toml:"scale"` + XOffset float64 `toml:"x_offset"` + YOffset float64 `toml:"y_offset"` +} + +// Sequence of fractal parameters +type FractalParameters struct { + Parameters []FractalParameter `toml:"fractal"` +} + +// LoadFractalParameters function reads fractal parameters from external text file +func LoadFractalParameters(filename string) (map[string]FractalParameter, error) { + var parameters FractalParameters + asMap := map[string]FractalParameter{} + + _, err := os.Stat(filename) + + if os.IsNotExist(err) { + return asMap, errors.New("Parameter file does not exist.") + } + if err != nil { + log.Fatal(err) + return asMap, err + } + + for _, parameter := range parameters.Parameters { + if _, exists := asMap[parameter.Name]; exists { + return asMap, fmt.Errorf( + "duplicate parameter name %q in %s", + parameter.Name, filename) + } + if parameter.Palette.Name == "" { + parameter.Palette.Slope = 1 + } + asMap[parameter.Name] = parameter + } + return asMap, nil +} + +// Resolution describes the image dimensions in pixels. +type Resolution struct { + Width uint + Height uint +} + +// NewResolution constructs a Resolution with the given width and height. +// Width and height are expected to be positive numbers. +func NewResolution(width, height uint) (Resolution, error) { + // Check for zero dimensions + if width == 0 { + return Resolution{}, errors.New("width cannot be zero") + } + if height == 0 { + return Resolution{}, errors.New("height cannot be zero") + } + + // Check for reasonable maximum dimensions to prevent memory issues + const maxDimension = 65535 // 2^16 - 1, reasonable for image processing + if width > maxDimension { + return Resolution{}, fmt.Errorf("width %d exceeds maximum allowed dimension %d", width, maxDimension) + } + if height > maxDimension { + return Resolution{}, fmt.Errorf("height %d exceeds maximum allowed dimension %d", height, maxDimension) + } + + return Resolution{ + Width: width, + Height: height, + }, nil +} + +func getSteps( + params FractalParameter, + image Image) (float64, float64) { + stepX := float64(params.Xmax-params.Xmin) / float64(image.Resolution.Width) + stepY := float64(params.Ymax-params.Ymin) / float64(image.Resolution.Height) + return stepX, stepY +} + +func calcIndex(params FractalParameter, i uint) uint { + index := params.Palette.Shift + int(i)*params.Palette.Slope + if index < 0 { + return 0 + } + return uint(index) +} + +// Image structure +type Image struct { + Resolution Resolution + Z ZImage + R RImage + I IImage + RGBA *image.NRGBA +} + +// Image constructor +func New(width uint, height uint) (Image, error) { + resolution, err := NewResolution(width, height) + + if err != nil { + return Image{}, err + } + + return Image{ + Resolution: resolution, + Z: NewZImage(resolution), + R: NewRImage(resolution), + I: NewIImage(resolution), + }, nil +} + +// Palette represents color palette used to map fractal calculation result +// (number of iterations, for example) into RGB or RGBA color. Palettes have +// usually 256 records, but it can be more or less. +type RGBPalette [][]byte + +func (i *Image) ApplyPalette(palette RGBPalette) { + r := i.Resolution + i.RGBA = image.NewNRGBA(image.Rect(0, 0, int(r.Width), int(r.Height))) + + for y := 0; y < int(r.Height); y++ { + offset := i.RGBA.PixOffset(0, y) + for x := uint(0); x < r.Width; x++ { + index := byte(i.I[y][x]) + i.RGBA.Pix[offset] = palette[index][0] + offset++ + i.RGBA.Pix[offset] = palette[index][1] + offset++ + i.RGBA.Pix[offset] = palette[index][2] + offset++ + i.RGBA.Pix[offset] = 0xff + offset++ + } + } +} + +func (image *Image) RImage2IImage() { + r := image.Resolution + width := r.Width + height := r.Height + + min, max := image.R.minMax(width, height) + k := 255.0 / (max - min) + + for y := uint(0); y < height; y++ { + for x := uint(0); x < width; x++ { + f := float64(image.R[y][x]) + f -= min + f *= k + if f > 255.0 { + f = 255 + } + i := int(f) & 255 + image.Z[y][x] = ZPixel(complex(float32(x), float32(y))) + image.I[y][x] = IPixel(i) + } + } +} + +func (image *Image) RImage2IImageWithFactor(maxFactor float64) { + r := image.Resolution + width := r.Width + height := r.Height + + min, max := image.R.minMax(width, height) + max *= maxFactor + k := 255.0 / (max - min) + + for y := uint(0); y < height; y++ { + for x := uint(0); x < width; x++ { + f := float64(image.R[y][x]) + f -= min + f *= k + if f > 255.0 { + f = 255 + } + i := int(f) & 255 + image.Z[y][x] = ZPixel(complex(float32(x), float32(y))) + image.I[y][x] = IPixel(i) + } + } +} + +// CalcBarnsleyJuliaJ1 calculates Barnsley J1 Mandelbrot-like set +func CalcBarnsleyJuliaJ1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyJuliaJ2 calculates Barnsley J2 Mandelbrot-like set +func CalcBarnsleyJuliaJ2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyMandelbrotM1 calculates Barnsley M1 Mandelbrot-like set +func CalcBarnsleyMandelbrotM1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM2 calculates Barnsley M2 Mandelbrot-like set +func CalcBarnsleyMandelbrotM2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM3 calculates Barnsley M3 Mandelbrot-like set +func CalcBarnsleyMandelbrotM3( + params FractalParameter, + image Image) { + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx > 0 { + zxn = zx2 - zy2 - 1 + zyn = 2.0 * zx * zy + } else { + zxn = zx2 - zy2 - 1 + cx*zx + zyn = 2.0*zx*zy + cy*zx + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcJuliaFn calculates Julia set into the provided ZPixels +func CalcJuliaFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates classic Julia fractal +func CalcJulia( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + params.Cy0 + zx = zx2 - zy2 + params.Cx0 + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^3+c +func CalcJuliaZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^4+c +func CalcJuliaZ4( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcMandelLambda calculates Mandelbrot variant of Lambda fractal +func CalcMandelLambda( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = c * z * (1 - z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMagnet calculates Magnet Mandelbrot-like set +func CalcMagnet( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMagnet calculates Magnet Julia-like set +func CalcMagnetJulia( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += 4.0 / float64(image.Resolution.Width) + } + zy0 += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrot calculates Mandelbrot set into the provided ZPixels +func CalcMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + cy + zx = zx2 - zy2 + cx + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotComplex calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func CalcMandelbrotComplex( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + if cmplx.Abs(z) > float64(params.Bailout) { + break + } + z = z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ2pZ calculates Mandelbrot set z=z^2+z+c into the provided ZPixels +// Calculations use complex numbers +func CalcMandelbrotZ2pZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z + z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ2mZ calculates Mandelbrot set z=z^2-z+c into the provided ZPixels +// Calculations use complex numbers +func CalcMandelbrotZ2mZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z - z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ3 calculates Mandelbrot set z=z^3+c into the provided ZPixels +// Calculations use complex numbers +func CalcMandelbrotZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ4 calculates Mandelbrot set z=z^4+c into the provided ZPixels +// Calculations use complex numbers +func CalcMandelbrotZ4( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotFn calculates Mandelbrot set into the provided ZPixels +func CalcMandelbrotFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(i) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarM calculates Manowar Mandelbrot-like set +func CalcManowarM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z = c + var z1 = c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarJ calculates Manowar Julia-like set +func CalcManowarJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var z1 = z + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + i *= 3 + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcNewton calculates Newton fractal +func CalcNewton( + params FractalParameter, + image Image) { + + const Epsilon = 0.001 + + var RootX1 = 1.0 + var RootY1 = 0.0 + + var RootX2 = -0.5 + var RootY2 = math.Sqrt(3) / 2 + + var RootX3 = -0.5 + var RootY3 = -math.Sqrt(3) / 2 + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + zx := zx0 + zy := zy0 + i := uint(0) + + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := 2.0/3.0*zx + (zx2-zy2)/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zyn := 2.0/3.0*zy - 2.0*zx*zy/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zx = zxn + zy = zyn + if math.Hypot(zx-RootX1, zy-RootY1) < Epsilon { + break + } + if math.Hypot(zx-RootX2, zy-RootY2) < Epsilon { + i += 128 + break + } + if math.Hypot(zx-RootX3, zy-RootY3) < Epsilon { + i += 192 + break + } + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixJ calculates Phoenix Julia-like set +func CalcPhoenixJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixM calculates Phoenix Mandelbrot-like set +func CalcPhoenixM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcZPowerMandelbrot calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func CalcZPowerMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(cx, cy) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = cmplx.Pow(z, z) + z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +/* taken from Fractint */ + +var map1 = [...][3]byte{ + {255, 255, 255}, {224, 224, 224}, {216, 216, 216}, {208, 208, 208}, + {200, 200, 200}, {192, 192, 192}, {184, 184, 184}, {176, 176, 176}, + {168, 168, 168}, {160, 160, 160}, {152, 152, 152}, {144, 144, 144}, + {136, 136, 136}, {128, 128, 128}, {120, 120, 120}, {112, 112, 112}, + {104, 104, 104}, {96, 96, 96}, {88, 88, 88}, {80, 80, 80}, + {72, 72, 72}, {64, 64, 64}, {56, 56, 56}, {48, 48, 56}, + {40, 40, 56}, {32, 32, 56}, {24, 24, 56}, {16, 16, 56}, + {8, 8, 56}, {000, 000, 60}, {000, 000, 64}, {000, 000, 72}, + {000, 000, 80}, {000, 000, 88}, {000, 000, 96}, {000, 000, 104}, + {000, 000, 108}, {000, 000, 116}, {000, 000, 124}, {000, 000, 132}, + {000, 000, 140}, {000, 000, 148}, {000, 000, 156}, {000, 000, 160}, + {000, 000, 168}, {000, 000, 176}, {000, 000, 184}, {000, 000, 192}, + {000, 000, 200}, {000, 000, 204}, {000, 000, 212}, {000, 000, 220}, + {000, 000, 228}, {000, 000, 236}, {000, 000, 244}, {000, 000, 252}, + {000, 4, 252}, {4, 12, 252}, {8, 20, 252}, {12, 28, 252}, + {16, 36, 252}, {20, 44, 252}, {20, 52, 252}, {24, 60, 252}, + {28, 68, 252}, {32, 76, 252}, {36, 84, 252}, {40, 92, 252}, + {40, 100, 252}, {44, 108, 252}, {48, 116, 252}, {52, 120, 252}, + {56, 128, 252}, {60, 136, 252}, {60, 144, 252}, {64, 152, 252}, + {68, 160, 252}, {72, 168, 252}, {76, 176, 252}, {80, 184, 252}, + {80, 192, 252}, {84, 200, 252}, {88, 208, 252}, {92, 216, 252}, + {96, 224, 252}, {100, 232, 252}, {100, 228, 248}, {96, 224, 244}, + {92, 216, 240}, {88, 212, 236}, {88, 204, 232}, {84, 200, 228}, + {80, 192, 220}, {76, 188, 216}, {76, 180, 212}, {72, 176, 208}, + {68, 168, 204}, {64, 164, 200}, {64, 156, 196}, {60, 152, 188}, + {56, 144, 184}, {52, 140, 180}, {52, 132, 176}, {48, 128, 172}, + {44, 120, 168}, {40, 116, 160}, {40, 108, 156}, {36, 104, 152}, + {32, 96, 148}, {28, 92, 144}, {28, 84, 140}, {24, 80, 136}, + {20, 72, 128}, {16, 68, 124}, {16, 60, 120}, {12, 56, 116}, + {8, 48, 112}, {4, 44, 108}, {000, 36, 100}, {4, 36, 104}, + {12, 40, 108}, {16, 44, 116}, {24, 48, 120}, {28, 52, 128}, + {36, 56, 132}, {40, 60, 140}, {48, 64, 144}, {52, 64, 148}, + {60, 68, 156}, {64, 72, 160}, {72, 76, 168}, {76, 80, 172}, + {84, 84, 180}, {88, 88, 184}, {96, 92, 192}, {104, 100, 192}, + {112, 112, 196}, {124, 120, 200}, {132, 132, 204}, {144, 140, 208}, + {152, 152, 212}, {164, 160, 216}, {172, 172, 220}, {180, 180, 224}, + {192, 192, 228}, {200, 200, 232}, {212, 212, 236}, {220, 220, 240}, + {232, 232, 244}, {240, 240, 248}, {252, 252, 252}, {252, 240, 244}, + {252, 224, 232}, {252, 208, 224}, {252, 192, 212}, {252, 176, 204}, + {252, 160, 192}, {252, 144, 184}, {252, 128, 172}, {252, 112, 164}, + {252, 96, 152}, {252, 80, 144}, {252, 64, 132}, {252, 48, 124}, + {252, 32, 112}, {252, 16, 104}, {252, 000, 92}, {236, 000, 88}, + {228, 000, 88}, {216, 4, 84}, {204, 4, 80}, {192, 8, 76}, + {180, 8, 76}, {168, 12, 72}, {156, 16, 68}, {144, 16, 64}, + {132, 20, 60}, {124, 20, 60}, {112, 24, 56}, {100, 24, 52}, + {88, 28, 48}, {76, 32, 44}, {64, 32, 44}, {52, 36, 40}, + {40, 36, 36}, {28, 40, 32}, {16, 44, 28}, {20, 52, 32}, + {24, 60, 36}, {28, 68, 44}, {32, 76, 48}, {36, 88, 56}, + {40, 96, 60}, {44, 104, 64}, {48, 112, 72}, {52, 120, 76}, + {56, 132, 84}, {48, 136, 84}, {40, 144, 80}, {52, 148, 88}, + {68, 156, 100}, {80, 164, 112}, {96, 168, 124}, {108, 176, 136}, + {124, 184, 144}, {136, 192, 156}, {152, 196, 168}, {164, 204, 180}, + {180, 212, 192}, {192, 220, 200}, {208, 224, 212}, {220, 232, 224}, + {236, 240, 236}, {252, 248, 248}, {252, 252, 252}, {252, 252, 240}, + {252, 252, 228}, {252, 252, 216}, {248, 248, 204}, {248, 248, 192}, + {248, 248, 180}, {248, 248, 164}, {244, 244, 152}, {244, 244, 140}, + {244, 244, 128}, {244, 244, 116}, {240, 240, 104}, {240, 240, 92}, + {240, 240, 76}, {240, 240, 64}, {236, 236, 52}, {236, 236, 40}, + {236, 236, 28}, {236, 236, 16}, {232, 232, 0}, {232, 232, 12}, + {232, 232, 28}, {232, 232, 40}, {236, 236, 56}, {236, 236, 68}, + {236, 236, 84}, {236, 236, 96}, {240, 240, 112}, {240, 240, 124}, + {240, 240, 140}, {244, 244, 152}, {244, 244, 168}, {244, 244, 180}, + {244, 244, 196}, {248, 248, 208}, {248, 248, 224}, {248, 248, 236}, + {252, 252, 252}, {248, 248, 248}, {240, 240, 240}, {232, 232, 232}} + +var map2 = [...][3]byte{ + {255, 255, 255}, {224, 224, 224}, {216, 216, 216}, {208, 208, 208}, + {200, 200, 200}, {192, 192, 192}, {184, 184, 184}, {176, 176, 176}, + {168, 168, 168}, {160, 160, 160}, {152, 152, 152}, {144, 144, 144}, + {136, 136, 136}, {128, 128, 128}, {120, 120, 120}, {112, 112, 112}, + {104, 104, 104}, {96, 96, 96}, {88, 88, 88}, {80, 80, 80}, + {72, 72, 72}, {64, 64, 64}, {56, 56, 56}, {48, 48, 56}, + {40, 40, 56}, {32, 32, 56}, {24, 24, 56}, {16, 16, 56}, + {8, 8, 56}, {000, 000, 60}, {000, 000, 64}, {000, 000, 72}, + {000, 000, 80}, {000, 000, 88}, {000, 000, 96}, {000, 000, 104}, + {000, 000, 108}, {000, 000, 116}, {000, 000, 124}, {000, 000, 132}, + {000, 000, 140}, {000, 000, 148}, {000, 000, 156}, {000, 000, 160}, + {000, 000, 168}, {000, 000, 176}, {000, 000, 184}, {000, 000, 192}, + {000, 000, 200}, {000, 000, 204}, {000, 000, 212}, {000, 000, 220}, + {000, 000, 228}, {000, 000, 236}, {000, 000, 244}, {000, 000, 252}, + {000, 4, 252}, {4, 12, 252}, {8, 20, 252}, {12, 28, 252}, + {16, 36, 252}, {20, 44, 252}, {20, 52, 252}, {24, 60, 252}, + {28, 68, 252}, {32, 76, 252}, {36, 84, 252}, {40, 92, 252}, + {40, 100, 252}, {44, 108, 252}, {48, 116, 252}, {52, 120, 252}, + {56, 128, 252}, {60, 136, 252}, {60, 144, 252}, {64, 152, 252}, + {68, 160, 252}, {72, 168, 252}, {76, 176, 252}, {80, 184, 252}, + {80, 192, 252}, {84, 200, 252}, {88, 208, 252}, {92, 216, 252}, + {96, 224, 252}, {100, 232, 252}, {100, 228, 248}, {96, 224, 244}, + {92, 216, 240}, {88, 212, 236}, {88, 204, 232}, {84, 200, 228}, + {80, 192, 220}, {76, 188, 216}, {76, 180, 212}, {72, 176, 208}, + {68, 168, 204}, {64, 164, 200}, {64, 156, 196}, {60, 152, 188}, + {56, 144, 184}, {52, 140, 180}, {52, 132, 176}, {48, 128, 172}, + {44, 120, 168}, {40, 116, 160}, {40, 108, 156}, {36, 104, 152}, + {32, 96, 148}, {28, 92, 144}, {28, 84, 140}, {24, 80, 136}, + {20, 72, 128}, {16, 68, 124}, {16, 60, 120}, {12, 56, 116}, + {8, 48, 112}, {4, 44, 108}, {000, 36, 100}, {4, 36, 104}, + {12, 40, 108}, {16, 44, 116}, {24, 48, 120}, {28, 52, 128}, + {36, 56, 132}, {40, 60, 140}, {48, 64, 144}, {52, 64, 148}, + {60, 68, 156}, {64, 72, 160}, {72, 76, 168}, {76, 80, 172}, + {84, 84, 180}, {88, 88, 184}, {96, 92, 192}, {104, 100, 192}, + {112, 112, 196}, {124, 120, 200}, {132, 132, 204}, {144, 140, 208}, + {152, 152, 212}, {164, 160, 216}, {172, 172, 220}, {180, 180, 224}, + {192, 192, 228}, {200, 200, 232}, {212, 212, 236}, {220, 220, 240}, + {232, 232, 244}, {240, 240, 248}, {252, 252, 252}, {252, 240, 244}, + {252, 224, 232}, {252, 208, 224}, {252, 192, 212}, {252, 176, 204}, + {252, 160, 192}, {252, 144, 184}, {252, 128, 172}, {252, 112, 164}, + {252, 96, 152}, {252, 80, 144}, {252, 64, 132}, {252, 48, 124}, + {252, 32, 112}, {252, 16, 104}, {252, 000, 92}, {236, 000, 88}, + {228, 000, 88}, {216, 4, 84}, {204, 4, 80}, {192, 8, 76}, + {180, 8, 76}, {168, 12, 72}, {156, 16, 68}, {144, 16, 64}, + {132, 20, 60}, {124, 20, 60}, {112, 24, 56}, {100, 24, 52}, + {88, 28, 48}, {76, 32, 44}, {64, 32, 44}, {52, 36, 40}, + {40, 36, 36}, {28, 40, 32}, {16, 44, 28}, {20, 52, 32}, + {24, 60, 36}, {28, 68, 44}, {32, 76, 48}, {36, 88, 56}, + {40, 96, 60}, {44, 104, 64}, {48, 112, 72}, {52, 120, 76}, + {56, 132, 84}, {48, 136, 84}, {40, 144, 80}, {52, 148, 88}, + {68, 156, 100}, {80, 164, 112}, {96, 168, 124}, {108, 176, 136}, + {124, 184, 144}, {136, 192, 156}, {152, 196, 168}, {164, 204, 180}, + {180, 212, 192}, {192, 220, 200}, {208, 224, 212}, {220, 232, 224}, + {236, 240, 236}, {252, 248, 248}, {252, 252, 252}, {252, 252, 240}, + {252, 252, 228}, {252, 252, 216}, {248, 248, 204}, {248, 248, 192}, + {248, 248, 180}, {248, 248, 164}, {244, 244, 152}, {244, 244, 140}, + {244, 244, 128}, {244, 244, 116}, {240, 240, 104}, {240, 240, 92}, + {240, 240, 76}, {240, 240, 64}, {236, 236, 52}, {236, 236, 40}, + {236, 236, 28}, {236, 236, 16}, {232, 232, 0}, {232, 232, 12}, + {232, 232, 28}, {232, 232, 40}, {236, 236, 56}, {236, 236, 68}, + {236, 236, 84}, {236, 236, 96}, {240, 240, 112}, {240, 240, 124}, + {240, 240, 140}, {244, 244, 152}, {244, 244, 168}, {244, 244, 180}, + {244, 244, 196}, {248, 248, 208}, {248, 248, 224}, {248, 248, 236}, + {252, 252, 252}, {248, 248, 248}, {240, 240, 240}, {232, 232, 232}} + +// BMPImageWriter implements image.Writer interface, it writes BMP format +type BMPImageWriter struct{} + +// WriteImage writes an image represented by byte slice into file with BMP format. +func (writer BMPImageWriter) WriteImage(filename string, img image.Image) error { + file, err := os.Create(filename) + if err != nil { + log.Fatal(err) + } + defer file.Close() + + bounds := img.Bounds() + width := bounds.Max.X - bounds.Min.X + height := bounds.Max.Y - bounds.Min.Y + + bmpHeader := []byte{ + /* BMP header structure: */ + 0x42, 0x4d, /* magic number */ + 0x46, 0x00, 0x00, 0x00, /* size of header=70 bytes */ + 0x00, 0x00, /* unused */ + 0x00, 0x00, /* unused */ + 0x36, 0x00, 0x00, 0x00, /* 54 bytes - offset to data */ + 0x28, 0x00, 0x00, 0x00, /* 40 bytes - bytes in DIB header */ + 0x00, 0x00, 0x00, 0x00, /* width of bitmap */ + 0x00, 0x00, 0x00, 0x00, /* height of bitmap */ + 0x01, 0x0, /* 1 pixel plane */ + 0x18, 0x00, /* 24 bpp */ + 0x00, 0x00, 0x00, 0x00, /* no compression */ + 0x00, 0x00, 0x00, 0x00, /* size of pixel array */ + 0x13, 0x0b, 0x00, 0x00, /* 2835 pixels/meter */ + 0x13, 0x0b, 0x00, 0x00, /* 2835 pixels/meter */ + 0x00, 0x00, 0x00, 0x00, /* color palette */ + 0x00, 0x00, 0x00, 0x00, /* important colors */ + } + + bmpHeader[18] = byte(width & 0xff) + bmpHeader[19] = byte(width >> 8) + bmpHeader[20] = byte(width >> 16) + bmpHeader[21] = byte(width >> 24) + bmpHeader[22] = byte(height & 0xff) + bmpHeader[23] = byte(height >> 8) + bmpHeader[24] = byte(height >> 16) + bmpHeader[25] = byte(height >> 24) + + file.Write(bmpHeader) + + for y := range height { + for x := range width { + r, g, b, _ := img.At(x, y).RGBA() + // swap RGB + color := []byte{byte(b >> 8), byte(g >> 8), byte(r >> 8)} + file.Write(color) + } + } + // no error + return nil +} + +// NewBMPImageWriter is a constructor for BMP image writer +func NewBMPImageWriter() BMPImageWriter { + return BMPImageWriter{} +} + +// PPMImageWriter implements image.Writer interface, it writes into selected PPM format +type PPMImageWriter struct{} + +// WritePPMImage writes an image represented by standard image.Image structure into file with PPM format. +func (writer PPMImageWriter) WriteImage(filename string, img image.Image) error { + file, err := os.Create(filename) + if err != nil { + log.Fatal(err) + } + defer file.Close() + + bounds := img.Bounds() + width := bounds.Max.X - bounds.Min.X + height := bounds.Max.Y - bounds.Min.Y + + fmt.Fprintln(file, "P3") + fmt.Fprintf(file, "%d %d\n", width, height) + fmt.Fprintln(file, "255") + + for y := range height { + for x := range width { + r, g, b, _ := img.At(x, y).RGBA() + fmt.Fprintf(file, "%d %d %d\n", r>>8, g>>8, b>>8) + } + } + return nil +} + +// NewPPMImageWriter is a constructor for PPM image writer +func NewPPMImageWriter() PPMImageWriter { + return PPMImageWriter{} +} + +// TGAImageWriter implements image.Writer interface, it writes TGA format +type TGAImageWriter struct{} + +// WriteTGAImage writes an image represented by byte slice into file with TGA format. +func (writer TGAImageWriter) WriteImage(filename string, img image.Image) error { + file, err := os.Create(filename) + if err != nil { + log.Fatal(err) + } + defer file.Close() + + bounds := img.Bounds() + width := bounds.Max.X - bounds.Min.X + height := bounds.Max.Y - bounds.Min.Y + + tgaHeader := []byte{ + /* TGA header structure: */ + 0x00, /* without image ID */ + 0x00, /* color map type: without palette */ + 0x02, /* uncompressed true color image */ + 0x00, 0x00, /* start of color palette (it is not used) */ + 0x00, 0x00, /* length of color palette (it is not used) */ + 0x00, /* bits per palette entry */ + 0x00, 0x00, 0x00, 0x00, /* image coordinates */ + 0x00, 0x00, /* image width */ + 0x00, 0x00, /* image height */ + 0x18, /* bits per pixel = 24 */ + 0x20, /* picture orientation: top-left origin */ + } + + /* image size is specified in TGA header */ + tgaHeader[12] = byte(width & 0xff) + tgaHeader[13] = byte(width >> 8) + tgaHeader[14] = byte(height & 0xff) + tgaHeader[15] = byte(height >> 8) + + file.Write(tgaHeader) + + for y := range height { + for x := range width { + r, g, b, _ := img.At(x, y).RGBA() + // swap RGB + color := []byte{byte(b >> 8), byte(g >> 8), byte(r >> 8)} + file.Write(color) + } + } + // no error + return nil +} + +// NewTGAImageWriter is a constructor for TGA image writer +func NewTGAImageWriter() TGAImageWriter { + return TGAImageWriter{} +} + +type Float32Node struct { + Value float32 + Left *Float32Node + Right *Float32Node +} + +type Float32BinaryTree struct { + Root *Float32Node +} + +func (bt *Float32BinaryTree) Insert(value float32) { + node := &Float32Node{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertFloat32Node(bt.Root, node) + } +} + +func insertFloat32Node(node, newNode *Float32Node) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertFloat32Node(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertFloat32Node(node.Right, newNode) + } + } +} + +func printFloat32Tree(node *Float32Node, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printFloat32Tree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printFloat32Tree(node.Right, level) + } +} + +/* +func main() { +var bt Float32BinaryTree ; + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + printTree(bt.Root, 0) +} +*/ + +type Float64Node struct { + Value float64 + Left *Float64Node + Right *Float64Node +} + +type Float64BinaryTree struct { + Root *Float64Node +} + +func (bt *Float64BinaryTree) Insert(value float64) { + node := &Float64Node{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertFloat64Node(bt.Root, node) + } +} + +func insertFloat64Node(node, newNode *Float64Node) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertFloat64Node(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertFloat64Node(node.Right, newNode) + } + } +} + +func printFloat64Tree(node *Float64Node, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printFloat64Tree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printFloat64Tree(node.Right, level) + } +} + +/* +func main() { +var bt Float64BinaryTree ; + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + printTree(bt.Root, 0) +} +*/ + +type IntNode struct { + Value int + Left *IntNode + Right *IntNode +} + +type IntBinaryTree struct { + Root *IntNode +} + +func (bt *IntBinaryTree) Insert(value int) { + node := &IntNode{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertIntNode(bt.Root, node) + } +} + +func insertIntNode(node, newNode *IntNode) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertIntNode(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertIntNode(node.Right, newNode) + } + } +} + +func printIntTree(node *IntNode, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printIntTree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printIntTree(node.Right, level) + } +} + +/* +func main() { +var bt IntBinaryTree ; + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + printTree(bt.Root, 0) +} +*/ + +type Int16Node struct { + Value int16 + Left *Int16Node + Right *Int16Node +} + +type Int16BinaryTree struct { + Root *Int16Node +} + +func (bt *Int16BinaryTree) Insert(value int16) { + node := &Int16Node{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertInt16Node(bt.Root, node) + } +} + +func insertInt16Node(node, newNode *Int16Node) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertInt16Node(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertInt16Node(node.Right, newNode) + } + } +} + +func printInt16Tree(node *Int16Node, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printInt16Tree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printInt16Tree(node.Right, level) + } +} + +/* +func main() { +var bt Int16BinaryTree ; + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + printTree(bt.Root, 0) +} +*/ + +type Int32Node struct { + Value int32 + Left *Int32Node + Right *Int32Node +} + +type Int32BinaryTree struct { + Root *Int32Node +} + +func (bt *Int32BinaryTree) Insert(value int32) { + node := &Int32Node{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertInt32Node(bt.Root, node) + } +} + +func insertInt32Node(node, newNode *Int32Node) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertInt32Node(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertInt32Node(node.Right, newNode) + } + } +} + +func printInt32Tree(node *Int32Node, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printInt32Tree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printInt32Tree(node.Right, level) + } +} + +/* +func main() { +var bt Int32BinaryTree ; + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + printTree(bt.Root, 0) +} +*/ + +type Int64Node struct { + Value int64 + Left *Int64Node + Right *Int64Node +} + +type Int64BinaryTree struct { + Root *Int64Node +} + +func (bt *Int64BinaryTree) Insert(value int64) { + node := &Int64Node{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertInt64Node(bt.Root, node) + } +} + +func insertInt64Node(node, newNode *Int64Node) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertInt64Node(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertInt64Node(node.Right, newNode) + } + } +} + +func printInt64Tree(node *Int64Node, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printInt64Tree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printInt64Tree(node.Right, level) + } +} + +/* +func main() { +var bt Int64BinaryTree ; + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + printTree(bt.Root, 0) +} +*/ + +type Int8Node struct { + Value int8 + Left *Int8Node + Right *Int8Node +} + +type Int8BinaryTree struct { + Root *Int8Node +} + +func (bt *Int8BinaryTree) Insert(value int8) { + node := &Int8Node{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertInt8Node(bt.Root, node) + } +} + +func insertInt8Node(node, newNode *Int8Node) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertInt8Node(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertInt8Node(node.Right, newNode) + } + } +} + +func printInt8Tree(node *Int8Node, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printInt8Tree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printInt8Tree(node.Right, level) + } +} + +/* +func main() { +var bt Int8BinaryTree ; + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + printTree(bt.Root, 0) +} +*/ + +type UintNode struct { + Value uint + Left *UintNode + Right *UintNode +} + +type UintBinaryTree struct { + Root *UintNode +} + +func (bt *UintBinaryTree) Insert(value uint) { + node := &UintNode{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertUintNode(bt.Root, node) + } +} + +func insertUintNode(node, newNode *UintNode) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertUintNode(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertUintNode(node.Right, newNode) + } + } +} + +func printUintTree(node *UintNode, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printUintTree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printUintTree(node.Right, level) + } +} + +/* +func main() { +var bt UintBinaryTree ; + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + printTree(bt.Root, 0) +} +*/ + +type Uint16Node struct { + Value uint16 + Left *Uint16Node + Right *Uint16Node +} + +type Uint16BinaryTree struct { + Root *Uint16Node +} + +func (bt *Uint16BinaryTree) Insert(value uint16) { + node := &Uint16Node{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertUint16Node(bt.Root, node) + } +} + +func insertUint16Node(node, newNode *Uint16Node) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertUint16Node(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertUint16Node(node.Right, newNode) + } + } +} + +func printUint16Tree(node *Uint16Node, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printUint16Tree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printUint16Tree(node.Right, level) + } +} + +/* +func main() { +var bt Uint16BinaryTree ; + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + printTree(bt.Root, 0) +} +*/ + +type Uint32Node struct { + Value uint32 + Left *Uint32Node + Right *Uint32Node +} + +type Uint32BinaryTree struct { + Root *Uint32Node +} + +func (bt *Uint32BinaryTree) Insert(value uint32) { + node := &Uint32Node{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertUint32Node(bt.Root, node) + } +} + +func insertUint32Node(node, newNode *Uint32Node) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertUint32Node(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertUint32Node(node.Right, newNode) + } + } +} + +func printUint32Tree(node *Uint32Node, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printUint32Tree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printUint32Tree(node.Right, level) + } +} + +/* +func main() { +var bt Uint32BinaryTree ; + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + printTree(bt.Root, 0) +} +*/ + +type Uint64Node struct { + Value uint64 + Left *Uint64Node + Right *Uint64Node +} + +type Uint64BinaryTree struct { + Root *Uint64Node +} + +func (bt *Uint64BinaryTree) Insert(value uint64) { + node := &Uint64Node{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertUint64Node(bt.Root, node) + } +} + +func insertUint64Node(node, newNode *Uint64Node) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertUint64Node(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertUint64Node(node.Right, newNode) + } + } +} + +func printUint64Tree(node *Uint64Node, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printUint64Tree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printUint64Tree(node.Right, level) + } +} + +/* +func main() { +var bt Uint64BinaryTree ; + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + printTree(bt.Root, 0) +} +*/ + +type Uint8Node struct { + Value uint8 + Left *Uint8Node + Right *Uint8Node +} + +type Uint8BinaryTree struct { + Root *Uint8Node +} + +func (bt *Uint8BinaryTree) Insert(value uint8) { + node := &Uint8Node{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertUint8Node(bt.Root, node) + } +} + +func insertUint8Node(node, newNode *Uint8Node) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertUint8Node(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertUint8Node(node.Right, newNode) + } + } +} + +func printUint8Tree(node *Uint8Node, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printUint8Tree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printUint8Tree(node.Right, level) + } +} + +// main +func main() { + var bt Uint8BinaryTree + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + printUint8Tree(bt.Root, 0) +} + +// CalcBarnsleyJuliaJ1 calculates Barnsley J1 Mandelbrot-like set +func A1CalcBarnsleyJuliaJ1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyJuliaJ2 calculates Barnsley J2 Mandelbrot-like set +func A1CalcBarnsleyJuliaJ2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyMandelbrotM1 calculates Barnsley M1 Mandelbrot-like set +func A1CalcBarnsleyMandelbrotM1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM2 calculates Barnsley M2 Mandelbrot-like set +func A1CalcBarnsleyMandelbrotM2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM3 calculates Barnsley M3 Mandelbrot-like set +func A1CalcBarnsleyMandelbrotM3( + params FractalParameter, + image Image) { + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx > 0 { + zxn = zx2 - zy2 - 1 + zyn = 2.0 * zx * zy + } else { + zxn = zx2 - zy2 - 1 + cx*zx + zyn = 2.0*zx*zy + cy*zx + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcJuliaFn calculates Julia set into the provided ZPixels +func A1CalcJuliaFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates classic Julia fractal +func A1CalcJulia( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + params.Cy0 + zx = zx2 - zy2 + params.Cx0 + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^3+c +func A1CalcJuliaZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^4+c +func A1CalcJuliaZ4( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcMandelLambda calculates Mandelbrot variant of Lambda fractal +func A1CalcMandelLambda( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = c * z * (1 - z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMagnet calculates Magnet Mandelbrot-like set +func A1CalcMagnet( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMagnet calculates Magnet Julia-like set +func A1CalcMagnetJulia( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += 4.0 / float64(image.Resolution.Width) + } + zy0 += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrot calculates Mandelbrot set into the provided ZPixels +func A1CalcMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + cy + zx = zx2 - zy2 + cx + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotComplex calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A1CalcMandelbrotComplex( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + if cmplx.Abs(z) > float64(params.Bailout) { + break + } + z = z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ2pZ calculates Mandelbrot set z=z^2+z+c into the provided ZPixels +// Calculations use complex numbers +func A1CalcMandelbrotZ2pZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z + z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ2mZ calculates Mandelbrot set z=z^2-z+c into the provided ZPixels +// Calculations use complex numbers +func A1CalcMandelbrotZ2mZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z - z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ3 calculates Mandelbrot set z=z^3+c into the provided ZPixels +// Calculations use complex numbers +func A1CalcMandelbrotZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ4 calculates Mandelbrot set z=z^4+c into the provided ZPixels +// Calculations use complex numbers +func A1CalcMandelbrotZ4( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotFn calculates Mandelbrot set into the provided ZPixels +func A1CalcMandelbrotFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(i) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarM calculates Manowar Mandelbrot-like set +func A1CalcManowarM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z = c + var z1 = c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarJ calculates Manowar Julia-like set +func A1CalcManowarJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var z1 = z + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + i *= 3 + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcNewton calculates Newton fractal +func A1CalcNewton( + params FractalParameter, + image Image) { + + const Epsilon = 0.001 + + var RootX1 = 1.0 + var RootY1 = 0.0 + + var RootX2 = -0.5 + var RootY2 = math.Sqrt(3) / 2 + + var RootX3 = -0.5 + var RootY3 = -math.Sqrt(3) / 2 + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + zx := zx0 + zy := zy0 + i := uint(0) + + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := 2.0/3.0*zx + (zx2-zy2)/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zyn := 2.0/3.0*zy - 2.0*zx*zy/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zx = zxn + zy = zyn + if math.Hypot(zx-RootX1, zy-RootY1) < Epsilon { + break + } + if math.Hypot(zx-RootX2, zy-RootY2) < Epsilon { + i += 128 + break + } + if math.Hypot(zx-RootX3, zy-RootY3) < Epsilon { + i += 192 + break + } + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixJ calculates Phoenix Julia-like set +func A1CalcPhoenixJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixM calculates Phoenix Mandelbrot-like set +func A1CalcPhoenixM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcZPowerMandelbrot calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A1CalcZPowerMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(cx, cy) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = cmplx.Pow(z, z) + z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyJuliaJ1 calculates Barnsley J1 Mandelbrot-like set +func A2CalcBarnsleyJuliaJ1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyJuliaJ2 calculates Barnsley J2 Mandelbrot-like set +func A2CalcBarnsleyJuliaJ2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyMandelbrotM1 calculates Barnsley M1 Mandelbrot-like set +func A2CalcBarnsleyMandelbrotM1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM2 calculates Barnsley M2 Mandelbrot-like set +func A2CalcBarnsleyMandelbrotM2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM3 calculates Barnsley M3 Mandelbrot-like set +func A2CalcBarnsleyMandelbrotM3( + params FractalParameter, + image Image) { + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx > 0 { + zxn = zx2 - zy2 - 1 + zyn = 2.0 * zx * zy + } else { + zxn = zx2 - zy2 - 1 + cx*zx + zyn = 2.0*zx*zy + cy*zx + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcJuliaFn calculates Julia set into the provided ZPixels +func A2CalcJuliaFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates classic Julia fractal +func A2CalcJulia( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + params.Cy0 + zx = zx2 - zy2 + params.Cx0 + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^3+c +func A2CalcJuliaZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^4+c +func A2CalcJuliaZ4( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcMandelLambda calculates Mandelbrot variant of Lambda fractal +func A2CalcMandelLambda( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = c * z * (1 - z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMagnet calculates Magnet Mandelbrot-like set +func A2CalcMagnet( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMagnet calculates Magnet Julia-like set +func A2CalcMagnetJulia( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += 4.0 / float64(image.Resolution.Width) + } + zy0 += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrot calculates Mandelbrot set into the provided ZPixels +func A2CalcMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + cy + zx = zx2 - zy2 + cx + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotComplex calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A2CalcMandelbrotComplex( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + if cmplx.Abs(z) > float64(params.Bailout) { + break + } + z = z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ2pZ calculates Mandelbrot set z=z^2+z+c into the provided ZPixels +// Calculations use complex numbers +func A2CalcMandelbrotZ2pZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z + z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ2mZ calculates Mandelbrot set z=z^2-z+c into the provided ZPixels +// Calculations use complex numbers +func A2CalcMandelbrotZ2mZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z - z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ3 calculates Mandelbrot set z=z^3+c into the provided ZPixels +// Calculations use complex numbers +func A2CalcMandelbrotZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ4 calculates Mandelbrot set z=z^4+c into the provided ZPixels +// Calculations use complex numbers +func A2CalcMandelbrotZ4( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotFn calculates Mandelbrot set into the provided ZPixels +func A2CalcMandelbrotFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(i) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarM calculates Manowar Mandelbrot-like set +func A2CalcManowarM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z = c + var z1 = c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarJ calculates Manowar Julia-like set +func A2CalcManowarJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var z1 = z + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + i *= 3 + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcNewton calculates Newton fractal +func A2CalcNewton( + params FractalParameter, + image Image) { + + const Epsilon = 0.001 + + var RootX1 = 1.0 + var RootY1 = 0.0 + + var RootX2 = -0.5 + var RootY2 = math.Sqrt(3) / 2 + + var RootX3 = -0.5 + var RootY3 = -math.Sqrt(3) / 2 + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + zx := zx0 + zy := zy0 + i := uint(0) + + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := 2.0/3.0*zx + (zx2-zy2)/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zyn := 2.0/3.0*zy - 2.0*zx*zy/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zx = zxn + zy = zyn + if math.Hypot(zx-RootX1, zy-RootY1) < Epsilon { + break + } + if math.Hypot(zx-RootX2, zy-RootY2) < Epsilon { + i += 128 + break + } + if math.Hypot(zx-RootX3, zy-RootY3) < Epsilon { + i += 192 + break + } + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixJ calculates Phoenix Julia-like set +func A2CalcPhoenixJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixM calculates Phoenix Mandelbrot-like set +func A2CalcPhoenixM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcZPowerMandelbrot calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A2CalcZPowerMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(cx, cy) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = cmplx.Pow(z, z) + z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyJuliaJ1 calculates Barnsley J1 Mandelbrot-like set +func A3CalcBarnsleyJuliaJ1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyJuliaJ2 calculates Barnsley J2 Mandelbrot-like set +func A3CalcBarnsleyJuliaJ2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyMandelbrotM1 calculates Barnsley M1 Mandelbrot-like set +func A3CalcBarnsleyMandelbrotM1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM2 calculates Barnsley M2 Mandelbrot-like set +func A3CalcBarnsleyMandelbrotM2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM3 calculates Barnsley M3 Mandelbrot-like set +func A3CalcBarnsleyMandelbrotM3( + params FractalParameter, + image Image) { + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx > 0 { + zxn = zx2 - zy2 - 1 + zyn = 2.0 * zx * zy + } else { + zxn = zx2 - zy2 - 1 + cx*zx + zyn = 2.0*zx*zy + cy*zx + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcJuliaFn calculates Julia set into the provided ZPixels +func A3CalcJuliaFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates classic Julia fractal +func A3CalcJulia( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + params.Cy0 + zx = zx2 - zy2 + params.Cx0 + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^3+c +func A3CalcJuliaZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^4+c +func A3CalcJuliaZ4( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcMandelLambda calculates Mandelbrot variant of Lambda fractal +func A3CalcMandelLambda( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = c * z * (1 - z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMagnet calculates Magnet Mandelbrot-like set +func A3CalcMagnet( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMagnet calculates Magnet Julia-like set +func A3CalcMagnetJulia( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += 4.0 / float64(image.Resolution.Width) + } + zy0 += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrot calculates Mandelbrot set into the provided ZPixels +func A3CalcMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + cy + zx = zx2 - zy2 + cx + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotComplex calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A3CalcMandelbrotComplex( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + if cmplx.Abs(z) > float64(params.Bailout) { + break + } + z = z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ2pZ calculates Mandelbrot set z=z^2+z+c into the provided ZPixels +// Calculations use complex numbers +func A3CalcMandelbrotZ2pZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z + z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ2mZ calculates Mandelbrot set z=z^2-z+c into the provided ZPixels +// Calculations use complex numbers +func A3CalcMandelbrotZ2mZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z - z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ3 calculates Mandelbrot set z=z^3+c into the provided ZPixels +// Calculations use complex numbers +func A3CalcMandelbrotZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ4 calculates Mandelbrot set z=z^4+c into the provided ZPixels +// Calculations use complex numbers +func A3CalcMandelbrotZ4( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotFn calculates Mandelbrot set into the provided ZPixels +func A3CalcMandelbrotFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(i) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarM calculates Manowar Mandelbrot-like set +func A3CalcManowarM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z = c + var z1 = c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarJ calculates Manowar Julia-like set +func A3CalcManowarJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var z1 = z + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + i *= 3 + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcNewton calculates Newton fractal +func A3CalcNewton( + params FractalParameter, + image Image) { + + const Epsilon = 0.001 + + var RootX1 = 1.0 + var RootY1 = 0.0 + + var RootX2 = -0.5 + var RootY2 = math.Sqrt(3) / 2 + + var RootX3 = -0.5 + var RootY3 = -math.Sqrt(3) / 2 + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + zx := zx0 + zy := zy0 + i := uint(0) + + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := 2.0/3.0*zx + (zx2-zy2)/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zyn := 2.0/3.0*zy - 2.0*zx*zy/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zx = zxn + zy = zyn + if math.Hypot(zx-RootX1, zy-RootY1) < Epsilon { + break + } + if math.Hypot(zx-RootX2, zy-RootY2) < Epsilon { + i += 128 + break + } + if math.Hypot(zx-RootX3, zy-RootY3) < Epsilon { + i += 192 + break + } + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixJ calculates Phoenix Julia-like set +func A3CalcPhoenixJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixM calculates Phoenix Mandelbrot-like set +func A3CalcPhoenixM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcZPowerMandelbrot calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A3CalcZPowerMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(cx, cy) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = cmplx.Pow(z, z) + z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyJuliaJ1 calculates Barnsley J1 Mandelbrot-like set +func A4CalcBarnsleyJuliaJ1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyJuliaJ2 calculates Barnsley J2 Mandelbrot-like set +func A4CalcBarnsleyJuliaJ2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyMandelbrotM1 calculates Barnsley M1 Mandelbrot-like set +func A4CalcBarnsleyMandelbrotM1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM2 calculates Barnsley M2 Mandelbrot-like set +func A4CalcBarnsleyMandelbrotM2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM3 calculates Barnsley M3 Mandelbrot-like set +func A4CalcBarnsleyMandelbrotM3( + params FractalParameter, + image Image) { + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx > 0 { + zxn = zx2 - zy2 - 1 + zyn = 2.0 * zx * zy + } else { + zxn = zx2 - zy2 - 1 + cx*zx + zyn = 2.0*zx*zy + cy*zx + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcJuliaFn calculates Julia set into the provided ZPixels +func A4CalcJuliaFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates classic Julia fractal +func A4CalcJulia( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + params.Cy0 + zx = zx2 - zy2 + params.Cx0 + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^3+c +func A4CalcJuliaZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^4+c +func A4CalcJuliaZ4( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcMandelLambda calculates Mandelbrot variant of Lambda fractal +func A4CalcMandelLambda( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = c * z * (1 - z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMagnet calculates Magnet Mandelbrot-like set +func A4CalcMagnet( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMagnet calculates Magnet Julia-like set +func A4CalcMagnetJulia( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += 4.0 / float64(image.Resolution.Width) + } + zy0 += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrot calculates Mandelbrot set into the provided ZPixels +func A4CalcMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + cy + zx = zx2 - zy2 + cx + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotComplex calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A4CalcMandelbrotComplex( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + if cmplx.Abs(z) > float64(params.Bailout) { + break + } + z = z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ2pZ calculates Mandelbrot set z=z^2+z+c into the provided ZPixels +// Calculations use complex numbers +func A4CalcMandelbrotZ2pZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z + z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ2mZ calculates Mandelbrot set z=z^2-z+c into the provided ZPixels +// Calculations use complex numbers +func A4CalcMandelbrotZ2mZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z - z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ3 calculates Mandelbrot set z=z^3+c into the provided ZPixels +// Calculations use complex numbers +func A4CalcMandelbrotZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ4 calculates Mandelbrot set z=z^4+c into the provided ZPixels +// Calculations use complex numbers +func A4CalcMandelbrotZ4( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotFn calculates Mandelbrot set into the provided ZPixels +func A4CalcMandelbrotFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(i) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarM calculates Manowar Mandelbrot-like set +func A4CalcManowarM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z = c + var z1 = c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarJ calculates Manowar Julia-like set +func A4CalcManowarJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var z1 = z + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + i *= 3 + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcNewton calculates Newton fractal +func A4CalcNewton( + params FractalParameter, + image Image) { + + const Epsilon = 0.001 + + var RootX1 = 1.0 + var RootY1 = 0.0 + + var RootX2 = -0.5 + var RootY2 = math.Sqrt(3) / 2 + + var RootX3 = -0.5 + var RootY3 = -math.Sqrt(3) / 2 + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + zx := zx0 + zy := zy0 + i := uint(0) + + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := 2.0/3.0*zx + (zx2-zy2)/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zyn := 2.0/3.0*zy - 2.0*zx*zy/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zx = zxn + zy = zyn + if math.Hypot(zx-RootX1, zy-RootY1) < Epsilon { + break + } + if math.Hypot(zx-RootX2, zy-RootY2) < Epsilon { + i += 128 + break + } + if math.Hypot(zx-RootX3, zy-RootY3) < Epsilon { + i += 192 + break + } + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixJ calculates Phoenix Julia-like set +func A4CalcPhoenixJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixM calculates Phoenix Mandelbrot-like set +func A4CalcPhoenixM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcZPowerMandelbrot calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A4CalcZPowerMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(cx, cy) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = cmplx.Pow(z, z) + z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyJuliaJ1 calculates Barnsley J1 Mandelbrot-like set +func A5CalcBarnsleyJuliaJ1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyJuliaJ2 calculates Barnsley J2 Mandelbrot-like set +func A5CalcBarnsleyJuliaJ2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyMandelbrotM1 calculates Barnsley M1 Mandelbrot-like set +func A5CalcBarnsleyMandelbrotM1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM2 calculates Barnsley M2 Mandelbrot-like set +func A5CalcBarnsleyMandelbrotM2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM3 calculates Barnsley M3 Mandelbrot-like set +func A5CalcBarnsleyMandelbrotM3( + params FractalParameter, + image Image) { + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx > 0 { + zxn = zx2 - zy2 - 1 + zyn = 2.0 * zx * zy + } else { + zxn = zx2 - zy2 - 1 + cx*zx + zyn = 2.0*zx*zy + cy*zx + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcJuliaFn calculates Julia set into the provided ZPixels +func A5CalcJuliaFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates classic Julia fractal +func A5CalcJulia( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + params.Cy0 + zx = zx2 - zy2 + params.Cx0 + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^3+c +func A5CalcJuliaZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^4+c +func A5CalcJuliaZ4( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcMandelLambda calculates Mandelbrot variant of Lambda fractal +func A5CalcMandelLambda( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = c * z * (1 - z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMagnet calculates Magnet Mandelbrot-like set +func A5CalcMagnet( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMagnet calculates Magnet Julia-like set +func A5CalcMagnetJulia( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += 4.0 / float64(image.Resolution.Width) + } + zy0 += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrot calculates Mandelbrot set into the provided ZPixels +func A5CalcMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + cy + zx = zx2 - zy2 + cx + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotComplex calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A5CalcMandelbrotComplex( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + if cmplx.Abs(z) > float64(params.Bailout) { + break + } + z = z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ2pZ calculates Mandelbrot set z=z^2+z+c into the provided ZPixels +// Calculations use complex numbers +func A5CalcMandelbrotZ2pZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z + z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ2mZ calculates Mandelbrot set z=z^2-z+c into the provided ZPixels +// Calculations use complex numbers +func A5CalcMandelbrotZ2mZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z - z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ3 calculates Mandelbrot set z=z^3+c into the provided ZPixels +// Calculations use complex numbers +func A5CalcMandelbrotZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ4 calculates Mandelbrot set z=z^4+c into the provided ZPixels +// Calculations use complex numbers +func A5CalcMandelbrotZ4( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotFn calculates Mandelbrot set into the provided ZPixels +func A5CalcMandelbrotFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(i) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarM calculates Manowar Mandelbrot-like set +func A5CalcManowarM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z = c + var z1 = c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarJ calculates Manowar Julia-like set +func A5CalcManowarJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var z1 = z + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + i *= 3 + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcNewton calculates Newton fractal +func A5CalcNewton( + params FractalParameter, + image Image) { + + const Epsilon = 0.001 + + var RootX1 = 1.0 + var RootY1 = 0.0 + + var RootX2 = -0.5 + var RootY2 = math.Sqrt(3) / 2 + + var RootX3 = -0.5 + var RootY3 = -math.Sqrt(3) / 2 + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + zx := zx0 + zy := zy0 + i := uint(0) + + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := 2.0/3.0*zx + (zx2-zy2)/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zyn := 2.0/3.0*zy - 2.0*zx*zy/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zx = zxn + zy = zyn + if math.Hypot(zx-RootX1, zy-RootY1) < Epsilon { + break + } + if math.Hypot(zx-RootX2, zy-RootY2) < Epsilon { + i += 128 + break + } + if math.Hypot(zx-RootX3, zy-RootY3) < Epsilon { + i += 192 + break + } + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixJ calculates Phoenix Julia-like set +func A5CalcPhoenixJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixM calculates Phoenix Mandelbrot-like set +func A5CalcPhoenixM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcZPowerMandelbrot calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A5CalcZPowerMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(cx, cy) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = cmplx.Pow(z, z) + z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyJuliaJ1 calculates Barnsley J1 Mandelbrot-like set +func A6CalcBarnsleyJuliaJ1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyJuliaJ2 calculates Barnsley J2 Mandelbrot-like set +func A6CalcBarnsleyJuliaJ2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyMandelbrotM1 calculates Barnsley M1 Mandelbrot-like set +func A6CalcBarnsleyMandelbrotM1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM2 calculates Barnsley M2 Mandelbrot-like set +func A6CalcBarnsleyMandelbrotM2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM3 calculates Barnsley M3 Mandelbrot-like set +func A6CalcBarnsleyMandelbrotM3( + params FractalParameter, + image Image) { + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx > 0 { + zxn = zx2 - zy2 - 1 + zyn = 2.0 * zx * zy + } else { + zxn = zx2 - zy2 - 1 + cx*zx + zyn = 2.0*zx*zy + cy*zx + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcJuliaFn calculates Julia set into the provided ZPixels +func A6CalcJuliaFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates classic Julia fractal +func A6CalcJulia( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + params.Cy0 + zx = zx2 - zy2 + params.Cx0 + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^3+c +func A6CalcJuliaZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^4+c +func A6CalcJuliaZ4( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcMandelLambda calculates Mandelbrot variant of Lambda fractal +func A6CalcMandelLambda( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = c * z * (1 - z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMagnet calculates Magnet Mandelbrot-like set +func A6CalcMagnet( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMagnet calculates Magnet Julia-like set +func A6CalcMagnetJulia( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += 4.0 / float64(image.Resolution.Width) + } + zy0 += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrot calculates Mandelbrot set into the provided ZPixels +func A6CalcMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + cy + zx = zx2 - zy2 + cx + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotComplex calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A6CalcMandelbrotComplex( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + if cmplx.Abs(z) > float64(params.Bailout) { + break + } + z = z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ2pZ calculates Mandelbrot set z=z^2+z+c into the provided ZPixels +// Calculations use complex numbers +func A6CalcMandelbrotZ2pZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z + z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ2mZ calculates Mandelbrot set z=z^2-z+c into the provided ZPixels +// Calculations use complex numbers +func A6CalcMandelbrotZ2mZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z - z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ3 calculates Mandelbrot set z=z^3+c into the provided ZPixels +// Calculations use complex numbers +func A6CalcMandelbrotZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ4 calculates Mandelbrot set z=z^4+c into the provided ZPixels +// Calculations use complex numbers +func A6CalcMandelbrotZ4( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotFn calculates Mandelbrot set into the provided ZPixels +func A6CalcMandelbrotFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(i) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarM calculates Manowar Mandelbrot-like set +func A6CalcManowarM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z = c + var z1 = c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarJ calculates Manowar Julia-like set +func A6CalcManowarJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var z1 = z + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + i *= 3 + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcNewton calculates Newton fractal +func A6CalcNewton( + params FractalParameter, + image Image) { + + const Epsilon = 0.001 + + var RootX1 = 1.0 + var RootY1 = 0.0 + + var RootX2 = -0.5 + var RootY2 = math.Sqrt(3) / 2 + + var RootX3 = -0.5 + var RootY3 = -math.Sqrt(3) / 2 + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + zx := zx0 + zy := zy0 + i := uint(0) + + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := 2.0/3.0*zx + (zx2-zy2)/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zyn := 2.0/3.0*zy - 2.0*zx*zy/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zx = zxn + zy = zyn + if math.Hypot(zx-RootX1, zy-RootY1) < Epsilon { + break + } + if math.Hypot(zx-RootX2, zy-RootY2) < Epsilon { + i += 128 + break + } + if math.Hypot(zx-RootX3, zy-RootY3) < Epsilon { + i += 192 + break + } + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixJ calculates Phoenix Julia-like set +func A6CalcPhoenixJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixM calculates Phoenix Mandelbrot-like set +func A6CalcPhoenixM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcZPowerMandelbrot calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A6CalcZPowerMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(cx, cy) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = cmplx.Pow(z, z) + z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyJuliaJ1 calculates Barnsley J1 Mandelbrot-like set +func A7CalcBarnsleyJuliaJ1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyJuliaJ2 calculates Barnsley J2 Mandelbrot-like set +func A7CalcBarnsleyJuliaJ2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyMandelbrotM1 calculates Barnsley M1 Mandelbrot-like set +func A7CalcBarnsleyMandelbrotM1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM2 calculates Barnsley M2 Mandelbrot-like set +func A7CalcBarnsleyMandelbrotM2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM3 calculates Barnsley M3 Mandelbrot-like set +func A7CalcBarnsleyMandelbrotM3( + params FractalParameter, + image Image) { + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx > 0 { + zxn = zx2 - zy2 - 1 + zyn = 2.0 * zx * zy + } else { + zxn = zx2 - zy2 - 1 + cx*zx + zyn = 2.0*zx*zy + cy*zx + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcJuliaFn calculates Julia set into the provided ZPixels +func A7CalcJuliaFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates classic Julia fractal +func A7CalcJulia( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + params.Cy0 + zx = zx2 - zy2 + params.Cx0 + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^3+c +func A7CalcJuliaZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^4+c +func A7CalcJuliaZ4( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcMandelLambda calculates Mandelbrot variant of Lambda fractal +func A7CalcMandelLambda( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = c * z * (1 - z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMagnet calculates Magnet Mandelbrot-like set +func A7CalcMagnet( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMagnet calculates Magnet Julia-like set +func A7CalcMagnetJulia( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += 4.0 / float64(image.Resolution.Width) + } + zy0 += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrot calculates Mandelbrot set into the provided ZPixels +func A7CalcMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + cy + zx = zx2 - zy2 + cx + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotComplex calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A7CalcMandelbrotComplex( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + if cmplx.Abs(z) > float64(params.Bailout) { + break + } + z = z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ2pZ calculates Mandelbrot set z=z^2+z+c into the provided ZPixels +// Calculations use complex numbers +func A7CalcMandelbrotZ2pZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z + z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ2mZ calculates Mandelbrot set z=z^2-z+c into the provided ZPixels +// Calculations use complex numbers +func A7CalcMandelbrotZ2mZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z - z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ3 calculates Mandelbrot set z=z^3+c into the provided ZPixels +// Calculations use complex numbers +func A7CalcMandelbrotZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ4 calculates Mandelbrot set z=z^4+c into the provided ZPixels +// Calculations use complex numbers +func A7CalcMandelbrotZ4( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotFn calculates Mandelbrot set into the provided ZPixels +func A7CalcMandelbrotFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(i) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarM calculates Manowar Mandelbrot-like set +func A7CalcManowarM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z = c + var z1 = c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarJ calculates Manowar Julia-like set +func A7CalcManowarJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var z1 = z + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + i *= 3 + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcNewton calculates Newton fractal +func A7CalcNewton( + params FractalParameter, + image Image) { + + const Epsilon = 0.001 + + var RootX1 = 1.0 + var RootY1 = 0.0 + + var RootX2 = -0.5 + var RootY2 = math.Sqrt(3) / 2 + + var RootX3 = -0.5 + var RootY3 = -math.Sqrt(3) / 2 + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + zx := zx0 + zy := zy0 + i := uint(0) + + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := 2.0/3.0*zx + (zx2-zy2)/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zyn := 2.0/3.0*zy - 2.0*zx*zy/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zx = zxn + zy = zyn + if math.Hypot(zx-RootX1, zy-RootY1) < Epsilon { + break + } + if math.Hypot(zx-RootX2, zy-RootY2) < Epsilon { + i += 128 + break + } + if math.Hypot(zx-RootX3, zy-RootY3) < Epsilon { + i += 192 + break + } + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixJ calculates Phoenix Julia-like set +func A7CalcPhoenixJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixM calculates Phoenix Mandelbrot-like set +func A7CalcPhoenixM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcZPowerMandelbrot calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A7CalcZPowerMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(cx, cy) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = cmplx.Pow(z, z) + z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyJuliaJ1 calculates Barnsley J1 Mandelbrot-like set +func A8CalcBarnsleyJuliaJ1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyJuliaJ2 calculates Barnsley J2 Mandelbrot-like set +func A8CalcBarnsleyJuliaJ2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyMandelbrotM1 calculates Barnsley M1 Mandelbrot-like set +func A8CalcBarnsleyMandelbrotM1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM2 calculates Barnsley M2 Mandelbrot-like set +func A8CalcBarnsleyMandelbrotM2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM3 calculates Barnsley M3 Mandelbrot-like set +func A8CalcBarnsleyMandelbrotM3( + params FractalParameter, + image Image) { + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx > 0 { + zxn = zx2 - zy2 - 1 + zyn = 2.0 * zx * zy + } else { + zxn = zx2 - zy2 - 1 + cx*zx + zyn = 2.0*zx*zy + cy*zx + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcJuliaFn calculates Julia set into the provided ZPixels +func A8CalcJuliaFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates classic Julia fractal +func A8CalcJulia( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + params.Cy0 + zx = zx2 - zy2 + params.Cx0 + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^3+c +func A8CalcJuliaZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^4+c +func A8CalcJuliaZ4( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcMandelLambda calculates Mandelbrot variant of Lambda fractal +func A8CalcMandelLambda( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = c * z * (1 - z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMagnet calculates Magnet Mandelbrot-like set +func A8CalcMagnet( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMagnet calculates Magnet Julia-like set +func A8CalcMagnetJulia( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += 4.0 / float64(image.Resolution.Width) + } + zy0 += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrot calculates Mandelbrot set into the provided ZPixels +func A8CalcMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + cy + zx = zx2 - zy2 + cx + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotComplex calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A8CalcMandelbrotComplex( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + if cmplx.Abs(z) > float64(params.Bailout) { + break + } + z = z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ2pZ calculates Mandelbrot set z=z^2+z+c into the provided ZPixels +// Calculations use complex numbers +func A8CalcMandelbrotZ2pZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z + z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ2mZ calculates Mandelbrot set z=z^2-z+c into the provided ZPixels +// Calculations use complex numbers +func A8CalcMandelbrotZ2mZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z - z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ3 calculates Mandelbrot set z=z^3+c into the provided ZPixels +// Calculations use complex numbers +func A8CalcMandelbrotZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ4 calculates Mandelbrot set z=z^4+c into the provided ZPixels +// Calculations use complex numbers +func A8CalcMandelbrotZ4( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotFn calculates Mandelbrot set into the provided ZPixels +func A8CalcMandelbrotFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(i) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarM calculates Manowar Mandelbrot-like set +func A8CalcManowarM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z = c + var z1 = c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarJ calculates Manowar Julia-like set +func A8CalcManowarJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var z1 = z + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + i *= 3 + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcNewton calculates Newton fractal +func A8CalcNewton( + params FractalParameter, + image Image) { + + const Epsilon = 0.001 + + var RootX1 = 1.0 + var RootY1 = 0.0 + + var RootX2 = -0.5 + var RootY2 = math.Sqrt(3) / 2 + + var RootX3 = -0.5 + var RootY3 = -math.Sqrt(3) / 2 + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + zx := zx0 + zy := zy0 + i := uint(0) + + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := 2.0/3.0*zx + (zx2-zy2)/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zyn := 2.0/3.0*zy - 2.0*zx*zy/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zx = zxn + zy = zyn + if math.Hypot(zx-RootX1, zy-RootY1) < Epsilon { + break + } + if math.Hypot(zx-RootX2, zy-RootY2) < Epsilon { + i += 128 + break + } + if math.Hypot(zx-RootX3, zy-RootY3) < Epsilon { + i += 192 + break + } + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixJ calculates Phoenix Julia-like set +func A8CalcPhoenixJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixM calculates Phoenix Mandelbrot-like set +func A8CalcPhoenixM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcZPowerMandelbrot calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A8CalcZPowerMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + + for y := uint(0); y < image.Resolution.Height; y++ { + + var cx float64 = params.Xmin + + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(cx, cy) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = cmplx.Pow(z, z) + z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// +// finito +// diff --git a/tests/benchmarks/data/go_1000_lines.go b/tests/benchmarks/data/go_1000_lines.go new file mode 100644 index 000000000..fe8e54516 --- /dev/null +++ b/tests/benchmarks/data/go_1000_lines.go @@ -0,0 +1,1000 @@ +// 1000 lines of Go + +package main + +import ( + "errors" + "fmt" + "image" + "log" + "math" + "math/cmplx" + "os" +) + +// IImage is representation of raster image consisting of IPixels +type IImage [][]IPixel + +// NewIImage constructs new instance of ZImage +func NewIImage(resolution Resolution) IImage { + iimage := make([][]IPixel, resolution.Height) + for y := uint(0); y < resolution.Height; y++ { + iimage[y] = make([]IPixel, resolution.Width) + } + return iimage +} + +// IPixel is a representation of pixel as one unsigned integer value +type IPixel uint64 + +// RImage is representation of raster image consisting of RPixels +type RImage [][]RPixel + +// NewRImage constructs new instance of RImage +func NewRImage(resolution Resolution) RImage { + rimage := make([][]RPixel, resolution.Height) + for y := uint(0); y < resolution.Height; y++ { + rimage[y] = make([]RPixel, resolution.Width) + } + return rimage +} + +// calcuate minimum and maximum pixel value +func (image *RImage) minMax(width, height uint) (float64, float64) { + min := float64(math.Inf(1)) + max := float64(math.Inf(-1)) + + for j := range height { + for i := range width { + z := float64((*image)[j][i]) + if max < z { + max = z + } + if min > z { + min = z + } + } + } + return min, max +} + +// RPixel is a representation of pixel as one real value +type RPixel float64 + +// ZImage is representation of raster image consisting of ZPixels +type ZImage [][]ZPixel + +// NewZImage constructs new instance of ZImage +func NewZImage(resolution Resolution) ZImage { + zimage := make([][]ZPixel, resolution.Height) + for y := uint(0); y < resolution.Height; y++ { + zimage[y] = make([]ZPixel, resolution.Width) + } + return zimage +} + +// ZPixel is a representation of pixel in complex plane +type ZPixel complex128 + +// Palette structure +type Palette struct { + Name string `toml:"name"` + Shift int `toml:"shift"` + Slope int `toml:"slope"` +} + +// FractalParameter structure contains information about all fractal parameters. +type FractalParameter struct { + Name string `toml:"name"` + Type string `toml:"type"` + Class string `toml:"class"` + Cx0 float64 `toml:"cx0"` + Cy0 float64 `toml:"cy0"` + Palette Palette `toml:"palette"` + Maxiter uint `toml:"maxiter"` + Bailout uint `toml:"bailout"` + Function1 string `toml:"function1"` + Function2 string `toml:"function2"` + Xmin float64 `toml:"xmin"` + Ymin float64 `toml:"ymin"` + Xmax float64 `toml:"xmax"` + Ymax float64 `toml:"ymax"` + A float64 `toml:"A"` + B float64 `toml:"B"` + C float64 `toml:"C"` + D float64 `toml:"D"` + Scale float64 `toml:"scale"` + XOffset float64 `toml:"x_offset"` + YOffset float64 `toml:"y_offset"` +} + +// FractalParameter2 structure contains information about all fractal parameters. +type FractalParameter2 struct { + Name string `toml:"name"` + Type string `toml:"type"` + Class string `toml:"class"` + Cx0 float64 `toml:"cx0"` + Cy0 float64 `toml:"cy0"` + Palette Palette `toml:"palette"` + Maxiter uint `toml:"maxiter"` + Bailout uint `toml:"bailout"` + Function1 string `toml:"function1"` + Function2 string `toml:"function2"` + Xmin float64 `toml:"xmin"` + Ymin float64 `toml:"ymin"` + Xmax float64 `toml:"xmax"` + Ymax float64 `toml:"ymax"` + A float64 `toml:"A"` + B float64 `toml:"B"` + C float64 `toml:"C"` + D float64 `toml:"D"` + Scale float64 `toml:"scale"` + XOffset float64 `toml:"x_offset"` + YOffset float64 `toml:"y_offset"` +} + +// Sequence of fractal parameters +type FractalParameters struct { + Parameters []FractalParameter `toml:"fractal"` +} + +// LoadFractalParameters function reads fractal parameters from external text file +func LoadFractalParameters(filename string) (map[string]FractalParameter, error) { + var parameters FractalParameters + asMap := map[string]FractalParameter{} + + _, err := os.Stat(filename) + + if os.IsNotExist(err) { + return asMap, errors.New("Parameter file does not exist.") + } + if err != nil { + log.Fatal(err) + return asMap, err + } + + for _, parameter := range parameters.Parameters { + if _, exists := asMap[parameter.Name]; exists { + return asMap, fmt.Errorf( + "duplicate parameter name %q in %s", + parameter.Name, filename) + } + if parameter.Palette.Name == "" { + parameter.Palette.Slope = 1 + } + asMap[parameter.Name] = parameter + } + return asMap, nil +} + +// Resolution describes the image dimensions in pixels. +type Resolution struct { + Width uint + Height uint +} + +// NewResolution constructs a Resolution with the given width and height. +// Width and height are expected to be positive numbers. +func NewResolution(width, height uint) (Resolution, error) { + // check for zero dimensions + if width == 0 { + return Resolution{}, errors.New("width cannot be zero") + } + + // check for zero dimensions + if height == 0 { + return Resolution{}, errors.New("height cannot be zero") + } + + // Check for reasonable maximum dimensions to prevent memory issues + const maxDimension = 65535 // 2^16 - 1, reasonable for image processing + + if width > maxDimension { + return Resolution{}, fmt.Errorf("width %d exceeds maximum allowed dimension %d", width, maxDimension) + } + if height > maxDimension { + return Resolution{}, fmt.Errorf("height %d exceeds maximum allowed dimension %d", height, maxDimension) + } + + return Resolution{ + Width: width, + Height: height, + }, nil +} + +func getSteps( + params FractalParameter, + image Image) (float64, float64) { + stepX := float64(params.Xmax-params.Xmin) / float64(image.Resolution.Width) + stepY := float64(params.Ymax-params.Ymin) / float64(image.Resolution.Height) + return stepX, stepY +} + +func calcIndex(params FractalParameter, i uint) uint { + index := params.Palette.Shift + int(i)*params.Palette.Slope + if index < 0 { + return 0 + } + return uint(index) +} + +// Image structure +type Image struct { + Resolution Resolution + Z ZImage + R RImage + I IImage + RGBA *image.NRGBA +} + +// Image constructor +func New(width uint, height uint) (Image, error) { + resolution, err := NewResolution(width, height) + + if err != nil { + return Image{}, err + } + + return Image{ + Resolution: resolution, + Z: NewZImage(resolution), + R: NewRImage(resolution), + I: NewIImage(resolution), + }, nil +} + +// Palette represents color palette used to map fractal calculation result +// (number of iterations, for example) into RGB or RGBA color. Palettes have +// usually 256 records, but it can be more or less. +type RGBPalette [][]byte + +func (i *Image) ApplyPalette(palette RGBPalette) { + r := i.Resolution + i.RGBA = image.NewNRGBA(image.Rect(0, 0, int(r.Width), int(r.Height))) + + for y := 0; y < int(r.Height); y++ { + offset := i.RGBA.PixOffset(0, y) + for x := uint(0); x < r.Width; x++ { + index := byte(i.I[y][x]) + i.RGBA.Pix[offset] = palette[index][0] + offset++ + i.RGBA.Pix[offset] = palette[index][1] + offset++ + i.RGBA.Pix[offset] = palette[index][2] + offset++ + i.RGBA.Pix[offset] = 0xff + offset++ + } + } +} + +func (image *Image) RImage2IImage() { + r := image.Resolution + width := r.Width + height := r.Height + + min, max := image.R.minMax(width, height) + k := 255.0 / (max - min) + + for y := uint(0); y < height; y++ { + for x := uint(0); x < width; x++ { + f := float64(image.R[y][x]) + f -= min + f *= k + if f > 255.0 { + f = 255 + } + i := int(f) & 255 + image.Z[y][x] = ZPixel(complex(float32(x), float32(y))) + image.I[y][x] = IPixel(i) + } + } +} + +func (image *Image) RImage2IImageWithFactor(maxFactor float64) { + r := image.Resolution + width := r.Width + height := r.Height + + min, max := image.R.minMax(width, height) + max *= maxFactor + k := 255.0 / (max - min) + + for y := uint(0); y < height; y++ { + for x := uint(0); x < width; x++ { + f := float64(image.R[y][x]) + f -= min + f *= k + if f > 255.0 { + f = 255 + } + i := int(f) & 255 + image.Z[y][x] = ZPixel(complex(float32(x), float32(y))) + image.I[y][x] = IPixel(i) + } + } +} + +// CalcBarnsleyJuliaJ1 calculates Barnsley J1 Mandelbrot-like set +func CalcBarnsleyJuliaJ1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyJuliaJ2 calculates Barnsley J2 Mandelbrot-like set +func CalcBarnsleyJuliaJ2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyMandelbrotM1 calculates Barnsley M1 Mandelbrot-like set +func CalcBarnsleyMandelbrotM1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM2 calculates Barnsley M2 Mandelbrot-like set +func CalcBarnsleyMandelbrotM2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM3 calculates Barnsley M3 Mandelbrot-like set +func CalcBarnsleyMandelbrotM3( + params FractalParameter, + image Image) { + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx > 0 { + zxn = zx2 - zy2 - 1 + zyn = 2.0 * zx * zy + } else { + zxn = zx2 - zy2 - 1 + cx*zx + zyn = 2.0*zx*zy + cy*zx + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcJuliaFn calculates Julia set into the provided ZPixels +func CalcJuliaFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates classic Julia fractal +func CalcJulia( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + params.Cy0 + zx = zx2 - zy2 + params.Cx0 + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^3+c +func CalcJuliaZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^4+c +func CalcJuliaZ4( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcMandelLambda calculates Mandelbrot variant of Lambda fractal +func CalcMandelLambda( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = c * z * (1 - z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMagnet calculates Magnet Mandelbrot-like set +func CalcMagnet( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMagnet calculates Magnet Julia-like set +func CalcMagnetJulia( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += 4.0 / float64(image.Resolution.Width) + } + zy0 += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrot calculates Mandelbrot set into the provided ZPixels +func CalcMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + cy + zx = zx2 - zy2 + cx + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotComplex calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func CalcMandelbrotComplex( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + if cmplx.Abs(z) > float64(params.Bailout) { + break + } + z = z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ2pZ calculates Mandelbrot set z=z^2+z+c into the provided ZPixels +// Calculations use complex numbers +func CalcMandelbrotZ2pZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z + z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ2mZ calculates Mandelbrot set z=z^2-z+c into the provided ZPixels +// Calculations use complex numbers +func CalcMandelbrotZ2mZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z - z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ3 calculates Mandelbrot set z=z^3+c into the provided ZPixels +// Calculations use complex numbers +func CalcMandelbrotZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ4 calculates Mandelbrot set z=z^4+c into the provided ZPixels +// Calculations use complex numbers +func CalcMandelbrotZ4( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotFn calculates Mandelbrot set into the provided ZPixels +func CalcMandelbrotFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(i) + cx += stepX + } + cy += stepY + } +} + +// main function +func main() { + fmt.Println("1000 lines of Go") +} + +// +// finito +// diff --git a/tests/benchmarks/data/go_100_lines.go b/tests/benchmarks/data/go_100_lines.go new file mode 100644 index 000000000..2a94ce6f2 --- /dev/null +++ b/tests/benchmarks/data/go_100_lines.go @@ -0,0 +1,100 @@ +// 100 lines of go +package main + +import ( + "errors" + "fmt" + "log" + "os" +) + +// FractalParameter structure contains information about all fractal parameters. +type FractalParameter struct { + Name string `toml:"name"` + Type string `toml:"type"` + Class string `toml:"class"` + Cx0 float64 `toml:"cx0"` + Cy0 float64 `toml:"cy0"` + Palette Palette `toml:"palette"` + Maxiter uint `toml:"maxiter"` + Bailout uint `toml:"bailout"` + Function1 string `toml:"function1"` + Function2 string `toml:"function2"` + Xmin float64 `toml:"xmin"` + Ymin float64 `toml:"ymin"` + Xmax float64 `toml:"xmax"` + Ymax float64 `toml:"ymax"` + A float64 `toml:"A"` + Scale float64 `toml:"scale"` + XOffset float64 `toml:"x_offset"` + YOffset float64 `toml:"y_offset"` +} + +// Sequence of fractal parameters +type FractalParameters struct { + Parameters []FractalParameter `toml:"fractal"` +} + +// LoadFractalParameters function reads fractal parameters from external text file +func LoadFractalParameters(filename string) (map[string]FractalParameter, error) { + var parameters FractalParameters + asMap := map[string]FractalParameter{} + + _, err := os.Stat(filename) + + if os.IsNotExist(err) { + return asMap, errors.New("Parameter file does not exist.") + } + if err != nil { + log.Fatal(err) + return asMap, err + } + + for _, parameter := range parameters.Parameters { + if _, exists := asMap[parameter.Name]; exists { + return asMap, fmt.Errorf( + "duplicate parameter name %q in %s", + parameter.Name, filename) + } + if parameter.Palette.Name == "" { + parameter.Palette.Slope = 1 + } + asMap[parameter.Name] = parameter + } + return asMap, nil +} + +// Resolution describes the image dimensions in pixels. +type Resolution struct { + Width uint + Height uint +} + +// NewResolution constructs a Resolution with the given width and height. +// Width and height are expected to be positive numbers. +func NewResolution(width, height uint) (Resolution, error) { + // Check for zero dimensions + if width == 0 { + return Resolution{}, errors.New("width cannot be zero") + } + if height == 0 { + return Resolution{}, errors.New("height cannot be zero") + } + + const maxDimension = 65535 // 2^16 - 1, reasonable for image processing + if width > maxDimension { + return Resolution{}, fmt.Errorf("width %d exceeds maximum allowed dimension %d", width, maxDimension) + } + if height > maxDimension { + return Resolution{}, fmt.Errorf("height %d exceeds maximum allowed dimension %d", height, maxDimension) + } + + return Resolution{ + Width: width, + Height: height, + }, nil +} + +func main() { + fmt.Println("100 lines") +} diff --git a/tests/benchmarks/data/go_10_lines.go b/tests/benchmarks/data/go_10_lines.go new file mode 100644 index 000000000..a776b41cd --- /dev/null +++ b/tests/benchmarks/data/go_10_lines.go @@ -0,0 +1,10 @@ +// 10 lines of go +package main + +import "fmt" + +func main() { + fmt.Println("10 lines") +} + +// finito diff --git a/tests/benchmarks/test_token_estimator.py b/tests/benchmarks/test_token_estimator.py index 9a79edcae..4a81a5212 100644 --- a/tests/benchmarks/test_token_estimator.py +++ b/tests/benchmarks/test_token_estimator.py @@ -435,3 +435,59 @@ def test_javascript_source_10000_lines(benchmark: BenchmarkFixture) -> None: None """ benchmark_file_tokenization(benchmark, "js_10000_lines.js") + + +def test_go_source_10_lines(benchmark: BenchmarkFixture) -> None: + """Test tokenizing Go source code containing just 10 lines. + + Parameters: + ---------- + benchmark (BenchmarkFixture): pytest-benchmark fixture. + + Returns: + ------- + None + """ + benchmark_file_tokenization(benchmark, "go_10_lines.go") + + +def test_go_source_100_lines(benchmark: BenchmarkFixture) -> None: + """Test tokenizing Go source code containing just 100 lines. + + Parameters: + ---------- + benchmark (BenchmarkFixture): pytest-benchmark fixture. + + Returns: + ------- + None + """ + benchmark_file_tokenization(benchmark, "go_100_lines.go") + + +def test_go_source_1000_lines(benchmark: BenchmarkFixture) -> None: + """Test tokenizing Go source code containing just 1000 lines. + + Parameters: + ---------- + benchmark (BenchmarkFixture): pytest-benchmark fixture. + + Returns: + ------- + None + """ + benchmark_file_tokenization(benchmark, "go_1000_lines.go") + + +def test_go_source_10000_lines(benchmark: BenchmarkFixture) -> None: + """Test tokenizing Go source code containing just 10000 lines. + + Parameters: + ---------- + benchmark (BenchmarkFixture): pytest-benchmark fixture. + + Returns: + ------- + None + """ + benchmark_file_tokenization(benchmark, "go_10000_lines.go") From 986c3723ae6c8d654dd0c1db5955319749714bd0 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Sun, 13 Sep 2026 09:32:43 +0200 Subject: [PATCH 081/120] Updated benchmarks results --- docs/benchmarks/tokenizer/10000_lines.svg | 298 ++--- docs/benchmarks/tokenizer/10000_lines.txt | 13 +- docs/benchmarks/tokenizer/1000_lines.svg | 351 +++--- docs/benchmarks/tokenizer/1000_lines.txt | 15 +- docs/benchmarks/tokenizer/100_lines.svg | 347 +++--- docs/benchmarks/tokenizer/100_lines.txt | 15 +- docs/benchmarks/tokenizer/10_lines.svg | 363 +++--- docs/benchmarks/tokenizer/10_lines.txt | 21 +- docs/benchmarks/tokenizer/all.svg | 1277 ++++++++++++--------- docs/benchmarks/tokenizer/all.txt | 70 +- 10 files changed, 1544 insertions(+), 1226 deletions(-) diff --git a/docs/benchmarks/tokenizer/10000_lines.svg b/docs/benchmarks/tokenizer/10000_lines.svg index 7c2b11558..11dda4264 100644 --- a/docs/benchmarks/tokenizer/10000_lines.svg +++ b/docs/benchmarks/tokenizer/10000_lines.svg @@ -1,14 +1,14 @@ - - + + -

Configuration

Optional explicit marker of the configuration format. When set, it must agree with the shape detected from the configuration body: ‘unified’ requires a synthesis input (a non-empty inference.providers, a -non-empty vector_store.providers, or a ogx.config block), -‘legacy’ requires no synthesis input. Reserved as the lever for a future -breaking change of the unified schema (R11). +non-empty vector_store.providers, or an ogx.config block), ‘legacy’ +requires no synthesis input. Reserved as the lever for a future breaking +change of the unified schema (R11). service @@ -642,9 +642,8 @@

Configuration

ogx - This section contains OGX configuration. Lightspeed Core -Stack service can call OGX in library mode or in server -mode. + This section contains OGX configuration. Lightspeed Core Stack +service can call OGX in library mode or in server mode. user_data_collection @@ -664,8 +663,8 @@

Configuration

MCP (Model Context Protocol) servers provide tools and capabilities to the AI agents. These are configured in this section. Only MCP servers defined in the lightspeed-stack.yaml configuration are available to the -agents. Tools configured in the OGX run.yaml are not accessible -to lightspeed-core agents. +agents. Tools configured in the OGX run.yaml are not accessible to +lightspeed-core agents. authentication @@ -945,8 +944,8 @@

FaissVectorStoreProvider

id string - OGX vector_io provider_id. Surrounding whitespace is -stripped before validation and emission. + OGX vector_io provider_id. Surrounding whitespace is stripped before +validation and emission. embedding_model @@ -989,6 +988,87 @@

FaissVectorStoreProviderConfig

+

GraniteGuardianConfig

+

Configuration for the Granite Guardian moderation guardrail.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
urlstringThe model_id to use for the guard
api_keystringAPI key for the inference
max_retriesintegerMaximun number of retires
timeoutintegerRequest timeout in seconds
verify_ssl + SSL certificate verification. Can be:
+
    +
  • True: Verify using system CA bundle (default, recommended)
  • +
  • False: Disable verification (insecure, for dev only)
  • +
  • str: Path to custom CA bundle file (for internal PKI) | | risks | +array | Risks to be considered while applying this guradrail |
  • +
+

GraniteGuardianShieldConfiguration

+

Configuration for a named Granite Guardian guardrail shield.

+

Attributes: name: Unique, user-facing name identifying this shield +instance. provider_id: Discriminator identifying this as a +granite-guardian shield. config: Granite-guardian-specific +configuration.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
namestringUnique, user-facing name identifying this shield instance.
provider_idstringDiscriminator identifying this as a granite-guardian shield.
config + Granite-guardian-specific configuration for this shield

InMemoryCacheConfig

In-memory cache configuration.

@@ -1220,116 +1300,13 @@

JwtRoleRule

-

OgxConfiguration

-

OGX configuration.

-

OGX is a comprehensive system that provides a uniform set of -tools for building, scaling, and deploying generative AI applications, -enabling developers to create, integrate, and orchestrate multiple AI -services and capabilities into an adaptable setup.

-

Useful resources:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
urlstringURL to OGX service; used when library mode is disabled. Must -be a valid HTTP or HTTPS URL.
api_keystringAPI key to access OGX service
use_as_library_clientbooleanWhen set to true OGX will be used in library mode, not in -server mode (default)
library_client_config_pathstringPath to configuration file used when OGX is run in library -mode. DEPRECATED legacy two-file setup: logs a startup warning since 0.6 -and is removed in 0.8 — use unified mode instead (the config block -below, and/or the root-level inference.providers section); migrate with -lightspeed-stack –migrate-config.
timeoutintegerTimeout in seconds for requests to OGX service. Default is -180 seconds (3 minutes) to accommodate long-running RAG queries.
max_retriesintegerMaximum number of connection attempts before giving up. Used on -startup to connect to OGX and retrieve its version. Connection -attempts are retried with a fixed delay to handle the case where Llama -Stack is still starting up (e.g., when running as a sidecar in the same -pod).
retry_delayintegerDelay in seconds between retry attempts. Used on startup to connect -to OGX and retrieve its version. Connection attempts are retried -with a fixed delay to handle the case where OGX is still -starting up (e.g., when running as a sidecar in the same pod).
allow_degraded_modebooleanIf enabled, Lightspeed Core can be started even when OGX is -not accessible (valid for server mode only)
config - Backend-specific knobs for unified mode, where LCORE synthesizes the -OGX run.yaml instead of reading an external file. Holds the -baseline selector, an optional profile path, and a raw native_override -escape hatch. Backend-agnostic high-level sections -(e.g. inference.providers) live at the configuration root, not here. -Mutually exclusive with library_client_config_path; that cross-field -check lives on the root Configuration model. When set in library mode, -library_client_config_path is not required.

ModelContextProtocolServer

Model context protocol server configuration.

MCP (Model Context Protocol) servers provide tools and capabilities to the AI agents. These are configured by this structure. Only MCP servers defined in the lightspeed-stack.yaml configuration are available -to the agents. Tools configured in the OGX run.yaml are not -accessible to lightspeed-core agents.

+to the agents. Tools configured in the OGX run.yaml are not accessible +to lightspeed-core agents.

Useful resources:

  • @@ -1407,8 +1384,8 @@

    ModelContextProtocolServer

    timeout integer Timeout in seconds for requests to the MCP server. If not specified, -the default timeout from OGX will be used. Note: This field is -reserved for future use when OGX adds timeout support. +the default timeout from OGX will be used. Note: This field is reserved +for future use when OGX adds timeout support. @@ -1440,6 +1417,108 @@

    ObservabilityConfiguration

    +

    OgxConfiguration

    +

    OGX configuration.

    +

    OGX is a comprehensive system that provides a uniform set of tools +for building, scaling, and deploying generative AI applications, +enabling developers to create, integrate, and orchestrate multiple AI +services and capabilities into an adaptable setup.

    +

    Useful resources:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldTypeDescription
    urlstringURL to OGX service; used when library mode is disabled. Must be a +valid HTTP or HTTPS URL.
    api_keystringAPI key to access OGX service
    use_as_library_clientbooleanWhen set to true OGX will be used in library mode, not in server +mode (default)
    library_client_config_pathstringPath to configuration file used when OGX is run in library mode. +DEPRECATED legacy two-file setup: logs a startup warning since 0.6 and +is removed in 0.8 — use unified mode instead (the config block below, +and/or the root-level inference.providers section); migrate with +lightspeed-stack –migrate-config.
    timeoutintegerTimeout in seconds for requests to OGX service. Default is 180 +seconds (3 minutes) to accommodate long-running RAG queries.
    max_retriesintegerMaximum number of connection attempts before giving up. Used on +startup to connect to OGX and retrieve its version. Connection attempts +are retried with a fixed delay to handle the case where OGX is still +starting up (e.g., when running as a sidecar in the same pod).
    retry_delayintegerDelay in seconds between retry attempts. Used on startup to connect +to OGX and retrieve its version. Connection attempts are retried with a +fixed delay to handle the case where OGX is still starting up (e.g., +when running as a sidecar in the same pod).
    allow_degraded_modebooleanIf enabled, Lightspeed Core can be started even when OGX is not +accessible (valid for server mode only)
    config + Backend-specific knobs for unified mode, where LCORE synthesizes the +OGX run.yaml instead of reading an external file. Holds the baseline +selector, an optional profile path, and a raw native_override escape +hatch. Backend-agnostic high-level sections (e.g. inference.providers) +live at the configuration root, not here. Mutually exclusive with +library_client_config_path; that cross-field check lives on the root +Configuration model. When set in library mode, +library_client_config_path is not required.

    OkpConfiguration

    OKP (Offline Knowledge Portal) provider configuration.

    Controls provider-specific behaviour for the OKP vector store. Only @@ -1514,8 +1593,8 @@

    PgvectorVectorStoreProvider

    id string - OGX vector_io provider_id. Surrounding whitespace is -stripped before validation and emission. + OGX vector_io provider_id. Surrounding whitespace is stripped before +validation and emission. embedding_model @@ -2237,6 +2316,76 @@

    RetrievalStrategyConfiguration

    +

    RiskDefinition

    +

    Definition for a custom risk category.

    +

    Custom risks allow applications to add use-case-specific safety +checks beyond the standard harm, jailbreak, leetspeak, amnesia, and +history_politics checks. Example: liability_risk = RiskDefinition( +name=“liability”, description=“Content requesting legal, medical, or +financial advice”, threshold=0.55, points=[“input”], ) pii_risk = +RiskDefinition( name=“pii_request”, description=“User is asking the AI +to reveal personal information”, threshold=0.50, points=[“input”, +“tool”], ) Note: To enable think mode (detailed reasoning) for a risk, +add the risk name to the thinking_enabled list in +ModerationConfig. Do not set enable_thinking +directly - it is managed internally.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldTypeDescription
    namestringUnique identifier for this risk (e.g., ‘liability’, +‘competitor_mention’)
    descriptionstringRisk definition text passed to Granite Guardian as +custom_criteria
    thresholdnumberScore threshold for flagging (lower = more sensitive)
    enabledbooleanWhether to run this check
    enable_thinkingbooleanInternal field - set via ModerationConfig.thinking_enabled list, not +directly. When True, Granite Guardian provides detailed reasoning before +scoring.
    pointsarrayWhere this risk is evaluated: input (user message), +output (model response), or tool (tool/MCP +content).
    violation_messagestringMessage to be displayed when this risk is violated

    RlsapiV1Configuration

    Configuration for the rlsapi v1 /infer endpoint.

    Settings specific to the RHEL Lightspeed Command Line Assistant (CLA) @@ -2384,26 +2533,55 @@

    ServiceConfiguration

    Number of Uvicorn worker processes to start + max_concurrent_file_uploads + integer + Maximum number of file uploads (POST /v1/files) processed +concurrently per worker. Each in-flight upload can hold up to the +configured maximum file size in memory, so this bounds worst-case memory +usage from concurrent uploads. Additional uploads are rejected with 429 +until a slot frees up. + + + max_concurrent_vector_store_attaches + integer + Maximum number of vector store file attachments (POST +/v1/vector-stores/{id}/files) processed concurrently per worker. Each +in-flight attachment re-reads and chunks the source file, so this bounds +worst-case memory usage independently of max_concurrent_file_uploads. +Additional attachments are rejected with 429 until a slot frees up. + + + delete_file_after_vector_store_attach + boolean + When true, deletes a file (POST /v1/files) once it has been +successfully attached to a vector store, since the vector store keeps +its own chunked/embedded copy of the content. Defaults to false to match +the OpenAI Files API, where a file remains reusable across multiple +vector stores until the caller explicitly deletes it - enabling this +makes attached files single-use: re-attaching the same file_id to +another vector store will fail once it has been deleted. + + color_log boolean Enables colorized logging - + access_log boolean Enables logging of all access information - + tls_config Transport Layer Security configuration for HTTPS support - + root_path string ASGI root path for serving behind a reverse proxy on a subpath - + cors Cross-Origin Resource Sharing configuration for cross-domain @@ -2621,26 +2799,25 @@

    TrustedProxyServiceAccount

    UnifiedInferenceProvider

    A high-level inference provider entry for unified-mode synthesis.

    Operators describe inference providers at this high level -(backend-agnostic vocabulary) instead of authoring raw OGX -provider blocks. The synthesizer -(apply_high_level_inference) expands each entry into a -OGX providers.inference entry, mapping -type to a provider_type and emitting +(backend-agnostic vocabulary) instead of authoring raw OGX provider +blocks. The synthesizer (apply_high_level_inference) +expands each entry into an OGX providers.inference entry, +mapping type to a provider_type and emitting ${env.<VAR>} references for secrets (never literal values).

    Attributes: type: Canonical provider identifier. Vendor-neutral so it survives a future backend change; each backend-specific synthesizer maps it to its own provider vocabulary. id: Optional identifier emitted as -the OGX provider_id. When omitted, synthesized as type with -underscores hyphenated. If set, must be non-empty after stripping -whitespace and may contain only lowercase letters, digits, underscores, -and hyphens. api_key_env: Name of the environment variable holding the -provider API key. Emitted verbatim as ${env.<name>} -so the secret never lands on disk resolved. allowed_models: Optional -allow-list of model identifiers passed through to the synthesized -provider config. extra: Additional provider-config keys merged verbatim -into the synthesized provider’s config block — an escape -hatch for provider-specific knobs not modeled here.

    +the OGX provider_id. When omitted, synthesized as type with underscores +hyphenated. If set, must be non-empty after stripping whitespace and may +contain only lowercase letters, digits, underscores, and hyphens. +api_key_env: Name of the environment variable holding the provider API +key. Emitted verbatim as ${env.<name>} so the secret +never lands on disk resolved. allowed_models: Optional allow-list of +model identifiers passed through to the synthesized provider config. +extra: Additional provider-config keys merged verbatim into the +synthesized provider’s config block — an escape hatch for +provider-specific knobs not modeled here.

    @@ -2658,15 +2835,15 @@

    UnifiedInferenceProvider

    - + - @@ -2693,19 +2870,20 @@

    UnifiedOgxConfig

    Backend-specific knobs for unified-mode OGX synthesis.

    Per Decision S5 of the design spike, backend-agnostic high-level sections (inference, …) live at the configuration root, not here. This -block holds only the OGX-specific synthesis controls: which -baseline to start from, an optional profile file, and a raw -native_override escape hatch.

    +block holds only the OGX-specific synthesis controls: which baseline to +start from, an optional profile file, and a raw native_override escape +hatch.

    Attributes: baseline: Synthesis starting point. “default” begins from -LCORE’s built-in baseline (src/data/default_run.yaml); “empty” begins -from an empty dict (used by the migration tool for an exact round-trip). -Ignored when profile is set. profile: Optional path to a -user-authored run.yaml-shaped file used as the synthesis baseline. -Relative paths resolve against the directory of the loaded -lightspeed-stack.yaml. native_override: Raw OGX schema -deep-merged last (maps merge recursively, lists and scalars replace). -The escape hatch for anything the high-level sections do not -express.

    +LCORE’s built-in baseline (src/data/default_run.yaml) including the +conditional OpenAI inference provider. “byo-llm” begins from the same +file with that OpenAI row removed. “empty” begins from an empty dict +(used by the migration tool for an exact round-trip). Ignored when +profile is set. profile: Optional path to a user-authored +run.yaml-shaped file used as the synthesis baseline. Relative paths +resolve against the directory of the loaded lightspeed-stack.yaml. +native_override: Raw OGX schema deep-merged last (maps merge +recursively, lists and scalars replace). The escape hatch for anything +the high-level sections do not express.

    type stringCanonical, backend-agnostic provider identifier mapped to a Llama -Stack provider_type by the synthesizer.Canonical, backend-agnostic provider identifier mapped to an OGX +provider_type by the synthesizer.
    id stringOptional identifier emitted as the OGX provider_id. When -omitted, synthesized as type with underscores hyphenated. If set, must -be non-empty after stripping whitespace and may contain only lowercase + Optional identifier emitted as the OGX provider_id. When omitted, +synthesized as type with underscores hyphenated. If set, must be +non-empty after stripping whitespace and may contain only lowercase letters, digits, underscores, and hyphens.
    @@ -2723,8 +2901,10 @@

    UnifiedOgxConfig

    - + @@ -2735,8 +2915,8 @@

    UnifiedOgxConfig

    - +
    baseline stringSynthesis starting point: ‘default’ uses LCORE’s built-in baseline, -‘empty’ starts from {}. Ignored when ‘profile’ is set.Synthesis starting point: ‘default’ uses LCORE’s built-in baseline +including the conditional OpenAI provider, ‘byo-llm’ uses the same +baseline without that OpenAI row, ‘empty’ starts from {}. Ignored when +‘profile’ is set.
    profile
    native_override objectRaw OGX schema deep-merged last (maps merge recursively; -lists and scalars replace).Raw OGX schema deep-merged last (maps merge recursively; lists and +scalars replace).
    @@ -2788,8 +2968,8 @@

    VectorStoreConfiguration

    sibling default_provider pointer, rather than a per-entry default flag.

    Attributes: default_provider: Provider id used for -vector_stores.default_* in the synthesized OGX config. Required -when providers is non-empty; must match one of providers[].id. Must be +vector_stores.default_* in the synthesized OGX config. Required when +providers is non-empty; must match one of providers[].id. Must be omitted when providers is empty. providers: Dynamic vector-store provider capacity for runtime POST /v1/vector-stores creates. Not the same as rag.byok.stores (static registered corpora).

    @@ -2810,9 +2990,9 @@

    VectorStoreConfiguration

    default_provider string - Provider id used for vector_stores.default_* in the synthesized -OGX config. Required when providers is non-empty; must match one -of providers[].id. + Provider id used for vector_stores.default_* in the synthesized OGX +config. Required when providers is non-empty; must match one of +providers[].id. providers diff --git a/docs/user_doc/config.json b/docs/user_doc/config.json index 7e3a918c7..1eef80d5e 100644 --- a/docs/user_doc/config.json +++ b/docs/user_doc/config.json @@ -604,6 +604,7 @@ "items": { "discriminator": { "mapping": { + "granite_guardian": "`#/components/schemas/`GraniteGuardianShieldConfiguration", "question_validity": "`#/components/schemas/`QuestionValidityShieldConfiguration", "redaction": "`#/components/schemas/`RedactionShieldConfiguration" }, @@ -615,6 +616,9 @@ }, { "$ref": "`#/components/schemas/`RedactionShieldConfiguration" + }, + { + "$ref": "`#/components/schemas/`GraniteGuardianShieldConfiguration" } ] }, @@ -863,6 +867,96 @@ "title": "FaissVectorStoreProviderConfig", "type": "object" }, + "GraniteGuardianConfig": { + "additionalProperties": false, + "description": "Configuration for the Granite Guardian moderation guardrail.", + "properties": { + "url": { + "description": "The model_id to use for the guard", + "title": "Base URL", + "type": "string" + }, + "api_key": { + "type": "string", + "nullable": true, + "default": null, + "description": "API key for the inference", + "title": "Granite Guardian API key" + }, + "max_retries": { + "default": 2, + "description": "Maximun number of retires", + "minimum": 0, + "maximum": 5, + "title": "Max retries", + "type": "integer" + }, + "timeout": { + "default": 30, + "description": "Request timeout in seconds", + "minimum": 5, + "maximum": 300, + "title": "Timeout", + "type": "integer" + }, + "verify_ssl": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ], + "default": true, + "description": "SSL certificate verification. Can be:\n - True: Verify using system CA bundle (default, recommended)\n - False: Disable verification (insecure, for dev only)\n - str: Path to custom CA bundle file (for internal PKI)", + "title": "Verify SSL" + }, + "risks": { + "description": "Risks to be considered while applying this guradrail", + "items": { + "$ref": "`#/components/schemas/`RiskDefinition" + }, + "title": "Defined risks", + "type": "array" + } + }, + "required": [ + "url", + "risks" + ], + "title": "GraniteGuardianConfig", + "type": "object" + }, + "GraniteGuardianShieldConfiguration": { + "additionalProperties": false, + "description": "Configuration for a named Granite Guardian guardrail shield.\n\nAttributes:\n name: Unique, user-facing name identifying this shield instance.\n provider_id: Discriminator identifying this as a granite-guardian shield.\n config: Granite-guardian-specific configuration.", + "properties": { + "name": { + "description": "Unique, user-facing name identifying this shield instance.", + "title": "Shield name", + "type": "string" + }, + "provider_id": { + "const": "granite_guardian", + "description": "Discriminator identifying this as a granite-guardian shield.", + "title": "Shield provider id", + "type": "string" + }, + "config": { + "$ref": "`#/components/schemas/`GraniteGuardianConfig", + "description": "Granite-guardian-specific configuration for this shield", + "title": "Shield configuration" + } + }, + "required": [ + "name", + "provider_id", + "config" + ], + "title": "GraniteGuardianShieldConfiguration", + "type": "object" + }, "InMemoryCacheConfig": { "additionalProperties": false, "description": "In-memory cache configuration.", @@ -1037,83 +1131,6 @@ "title": "JwtRoleRule", "type": "object" }, - "OgxConfiguration": { - "additionalProperties": false, - "description": "OGX configuration.\n\nOGX is a comprehensive system that provides a uniform set of tools\nfor building, scaling, and deploying generative AI applications, enabling\ndevelopers to create, integrate, and orchestrate multiple AI services and\ncapabilities into an adaptable setup.\n\nUseful resources:\n\n - [OGX](https://ogx-ai.github.io/)\n - [Python OGX client](https://github.com/ogx-ai/ogx-client-python)\n - [Build AI Applications with OGX](https://ogx-ai.github.io/docs/building_applications)", - "properties": { - "url": { - "type": "string", - "nullable": true, - "default": null, - "description": "URL to OGX service; used when library mode is disabled. Must be a valid HTTP or HTTPS URL.", - "title": "OGX URL" - }, - "api_key": { - "type": "string", - "nullable": true, - "default": null, - "description": "API key to access OGX service", - "title": "API key" - }, - "use_as_library_client": { - "type": "boolean", - "nullable": true, - "default": null, - "description": "When set to true OGX will be used in library mode, not in server mode (default)", - "title": "Use as library" - }, - "library_client_config_path": { - "type": "string", - "nullable": true, - "default": null, - "description": "Path to configuration file used when OGX is run in library mode. DEPRECATED legacy two-file setup: logs a startup warning since 0.6 and is removed in 0.8 — use unified mode instead (the config block below, and/or the root-level inference.providers section); migrate with lightspeed-stack --migrate-config.", - "title": "OGX configuration path (legacy, deprecated)" - }, - "timeout": { - "default": 180, - "description": "Timeout in seconds for requests to OGX service. Default is 180 seconds (3 minutes) to accommodate long-running RAG queries.", - "minimum": 0, - "title": "Request timeout", - "type": "integer" - }, - "max_retries": { - "default": 5, - "description": "Maximum number of connection attempts before giving up. Used on startup to connect to OGX and retrieve its version. Connection attempts are retried with a fixed delay to handle the case where OGX is still starting up (e.g., when running as a sidecar in the same pod).", - "minimum": 0, - "title": "Maximum number of connection attempts before giving up", - "type": "integer" - }, - "retry_delay": { - "default": 2, - "description": "Delay in seconds between retry attempts. Used on startup to connect to OGX and retrieve its version. Connection attempts are retried with a fixed delay to handle the case where OGX is still starting up (e.g., when running as a sidecar in the same pod).", - "minimum": 0, - "title": "Delay in seconds between retry attempts", - "type": "integer" - }, - "allow_degraded_mode": { - "type": "boolean", - "nullable": true, - "default": false, - "description": "If enabled, Lightspeed Core can be started even when OGX is not accessible (valid for server mode only)", - "title": "Allow degraded mode" - }, - "config": { - "anyOf": [ - { - "$ref": "`#/components/schemas/`UnifiedOgxConfig" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Backend-specific knobs for unified mode, where LCORE synthesizes the OGX run.yaml instead of reading an external file. Holds the baseline selector, an optional profile path, and a raw native_override escape hatch. Backend-agnostic high-level sections (e.g. inference.providers) live at the configuration root, not here. Mutually exclusive with library_client_config_path; that cross-field check lives on the root Configuration model. When set in library mode, library_client_config_path is not required.", - "title": "Unified OGX configuration" - } - }, - "title": "OgxConfiguration", - "type": "object" - }, "ModelContextProtocolServer": { "additionalProperties": false, "description": "Model context protocol server configuration.\n\nMCP (Model Context Protocol) servers provide tools and capabilities to the\nAI agents. These are configured by this structure. Only MCP servers\ndefined in the lightspeed-stack.yaml configuration are available to the\nagents. Tools configured in the OGX run.yaml are not accessible to\nlightspeed-core agents.\n\nUseful resources:\n\n- [Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro)\n- [MCP FAQs](https://modelcontextprotocol.io/faqs)\n- [Wikipedia article](https://en.wikipedia.org/wiki/Model_Context_Protocol)", @@ -1198,6 +1215,83 @@ "title": "ObservabilityConfiguration", "type": "object" }, + "OgxConfiguration": { + "additionalProperties": false, + "description": "OGX configuration.\n\nOGX is a comprehensive system that provides a uniform set of tools\nfor building, scaling, and deploying generative AI applications, enabling\ndevelopers to create, integrate, and orchestrate multiple AI services and\ncapabilities into an adaptable setup.\n\nUseful resources:\n\n - [OGX](https://ogx-ai.github.io/)\n - [Python OGX client](https://github.com/ogx-ai/ogx-client-python)\n - [Build AI Applications with OGX](https://ogx-ai.github.io/docs/building_applications)", + "properties": { + "url": { + "type": "string", + "nullable": true, + "default": null, + "description": "URL to OGX service; used when library mode is disabled. Must be a valid HTTP or HTTPS URL.", + "title": "OGX URL" + }, + "api_key": { + "type": "string", + "nullable": true, + "default": null, + "description": "API key to access OGX service", + "title": "API key" + }, + "use_as_library_client": { + "type": "boolean", + "nullable": true, + "default": null, + "description": "When set to true OGX will be used in library mode, not in server mode (default)", + "title": "Use as library" + }, + "library_client_config_path": { + "type": "string", + "nullable": true, + "default": null, + "description": "Path to configuration file used when OGX is run in library mode. DEPRECATED legacy two-file setup: logs a startup warning since 0.6 and is removed in 0.8 — use unified mode instead (the config block below, and/or the root-level inference.providers section); migrate with lightspeed-stack --migrate-config.", + "title": "OGX configuration path (legacy, deprecated)" + }, + "timeout": { + "default": 180, + "description": "Timeout in seconds for requests to OGX service. Default is 180 seconds (3 minutes) to accommodate long-running RAG queries.", + "minimum": 0, + "title": "Request timeout", + "type": "integer" + }, + "max_retries": { + "default": 5, + "description": "Maximum number of connection attempts before giving up. Used on startup to connect to OGX and retrieve its version. Connection attempts are retried with a fixed delay to handle the case where OGX is still starting up (e.g., when running as a sidecar in the same pod).", + "minimum": 0, + "title": "Maximum number of connection attempts before giving up", + "type": "integer" + }, + "retry_delay": { + "default": 2, + "description": "Delay in seconds between retry attempts. Used on startup to connect to OGX and retrieve its version. Connection attempts are retried with a fixed delay to handle the case where OGX is still starting up (e.g., when running as a sidecar in the same pod).", + "minimum": 0, + "title": "Delay in seconds between retry attempts", + "type": "integer" + }, + "allow_degraded_mode": { + "type": "boolean", + "nullable": true, + "default": false, + "description": "If enabled, Lightspeed Core can be started even when OGX is not accessible (valid for server mode only)", + "title": "Allow degraded mode" + }, + "config": { + "anyOf": [ + { + "$ref": "`#/components/schemas/`UnifiedOgxConfig" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Backend-specific knobs for unified mode, where LCORE synthesizes the OGX run.yaml instead of reading an external file. Holds the baseline selector, an optional profile path, and a raw native_override escape hatch. Backend-agnostic high-level sections (e.g. inference.providers) live at the configuration root, not here. Mutually exclusive with library_client_config_path; that cross-field check lives on the root Configuration model. When set in library mode, library_client_config_path is not required.", + "title": "Unified OGX configuration" + } + }, + "title": "OgxConfiguration", + "type": "object" + }, "OkpConfiguration": { "additionalProperties": false, "description": "OKP (Offline Knowledge Portal) provider configuration.\n\nControls provider-specific behaviour for the OKP vector store.\nOnly relevant when ``\"okp\"`` is listed in ``rag.retrieval.inline.sources``\nor ``rag.retrieval.tool.sources``.", @@ -1428,7 +1522,7 @@ "type": "string" }, "model_prompt": { - "default": "\nInstructions:\n- You are a question classifying tool\n- You are an expert in kubernetes and openshift\n- Your job is to determine where or a user's question is related to kubernetes and/or openshift technologies and to provide a one-word response.\n- If a question appears to be related to kubernetes or openshift technologies, answer with the word ${allowed}, otherwise answer with the word ${rejected}.\n- Do not explain your answer, just provide the one-word response. Do not give any other response.\n- If the given question is an empty string, answer with the word ${rejected}\n\n\nExample Question:\nWhy is the sky blue?\nExample Response:\n${rejected}\n\nExample Question:\nWhy is the grass green?\nExample Response:\n${rejected}\n\nExample Question:\nWhy is sand yellow?\nExample Response:\n${rejected}\n\nExample Question:\nCan you help configure my cluster to automatically scale?\nExample Response:\n${allowed}\n\nQuestion:\n${message}\nResponse:\n", + "default": "\nInstructions:\n- You are a question classifying tool\n- You are an expert in Kubernetes and OpenShift\n- Your job is to determine where or a user's question is related to Kubernetes and/or OpenShift technologies and to provide a one-word response.\n- If a question appears to be related to Kubernetes or OpenShift technologies, answer with the word ${allowed}, otherwise answer with the word ${rejected}.\n- Do not explain your answer, just provide the one-word response. Do not give any other response.\n- If the given question is an empty string, answer with the word ${rejected}\n\n\nExample Question:\nWhy is the sky blue?\nExample Response:\n${rejected}\n\nExample Question:\nWhy is the grass green?\nExample Response:\n${rejected}\n\nExample Question:\nWhy is sand yellow?\nExample Response:\n${rejected}\n\nExample Question:\nCan you help configure my cluster to automatically scale?\nExample Response:\n${allowed}\n\nQuestion:\n${message}\nResponse:\n", "description": "The default prompt sent to the LLM used to validate the Users' question.", "title": "Model prompt", "type": "string" @@ -1912,6 +2006,69 @@ "title": "RetrievalStrategyConfiguration", "type": "object" }, + "RiskDefinition": { + "additionalProperties": false, + "description": "Definition for a custom risk category.\n\nCustom risks allow applications to add use-case-specific safety checks\nbeyond the standard harm, jailbreak, leetspeak, amnesia, and\nhistory_politics checks.\nExample:\n liability_risk = RiskDefinition(\n name=\"liability\",\n description=\"Content requesting legal, medical, or financial advice\",\n threshold=0.55,\n points=[\"input\"],\n )\n pii_risk = RiskDefinition(\n name=\"pii_request\",\n description=\"User is asking the AI to reveal personal information\",\n threshold=0.50,\n points=[\"input\", \"tool\"],\n )\nNote:\n To enable think mode (detailed reasoning) for a risk, add the risk name\n to the `thinking_enabled` list in `ModerationConfig`. Do not set\n `enable_thinking` directly - it is managed internally.", + "properties": { + "name": { + "description": "Unique identifier for this risk (e.g., 'liability', 'competitor_mention')", + "title": "Risk name", + "type": "string" + }, + "description": { + "description": "Risk definition text passed to Granite Guardian as custom_criteria", + "title": "Rist description", + "type": "string" + }, + "threshold": { + "default": 0.65, + "description": "Score threshold for flagging (lower = more sensitive)", + "maximum": 1.0, + "minimum": 0.0, + "title": "Risk threshold", + "type": "number" + }, + "enabled": { + "default": true, + "description": "Whether to run this check", + "title": "Risk enabled", + "type": "boolean" + }, + "enable_thinking": { + "default": false, + "description": "Internal field - set via ModerationConfig.thinking_enabled list, not directly. When True, Granite Guardian provides detailed reasoning before scoring.", + "title": "Risk enable thinking", + "type": "boolean" + }, + "points": { + "description": "Where this risk is evaluated: `input` (user message), `output` (model response), or `tool` (tool/MCP content).", + "items": { + "enum": [ + "input", + "output", + "tool" + ], + "type": "string" + }, + "minItems": 1, + "title": "Guardrail points", + "type": "array" + }, + "violation_message": { + "description": "Message to be displayed when this risk is violated", + "title": "Violation message", + "type": "string" + } + }, + "required": [ + "name", + "description", + "points", + "violation_message" + ], + "title": "RiskDefinition", + "type": "object" + }, "RlsapiV1Configuration": { "additionalProperties": false, "description": "Configuration for the rlsapi v1 /infer endpoint.\n\nSettings specific to the RHEL Lightspeed Command Line Assistant (CLA)\nstateless inference endpoint. Kept separate from shared configuration\nsections so that CLA-specific options do not affect other endpoints.", @@ -2018,6 +2175,26 @@ "title": "Number of workers", "type": "integer" }, + "max_concurrent_file_uploads": { + "default": 5, + "description": "Maximum number of file uploads (POST /v1/files) processed concurrently per worker. Each in-flight upload can hold up to the configured maximum file size in memory, so this bounds worst-case memory usage from concurrent uploads. Additional uploads are rejected with 429 until a slot frees up.", + "minimum": 0, + "title": "Maximum concurrent file uploads", + "type": "integer" + }, + "max_concurrent_vector_store_attaches": { + "default": 5, + "description": "Maximum number of vector store file attachments (POST /v1/vector-stores/{id}/files) processed concurrently per worker. Each in-flight attachment re-reads and chunks the source file, so this bounds worst-case memory usage independently of max_concurrent_file_uploads. Additional attachments are rejected with 429 until a slot frees up.", + "minimum": 0, + "title": "Maximum concurrent vector store file attachments", + "type": "integer" + }, + "delete_file_after_vector_store_attach": { + "default": false, + "description": "When true, deletes a file (POST /v1/files) once it has been successfully attached to a vector store, since the vector store keeps its own chunked/embedded copy of the content. Defaults to false to match the OpenAI Files API, where a file remains reusable across multiple vector stores until the caller explicitly deletes it - enabling this makes attached files single-use: re-attaching the same file_id to another vector store will fail once it has been deleted.", + "title": "Delete file after vector store attach", + "type": "boolean" + }, "color_log": { "default": true, "description": "Enables colorized logging", @@ -2195,10 +2372,10 @@ }, "UnifiedInferenceProvider": { "additionalProperties": false, - "description": "A high-level inference provider entry for unified-mode synthesis.\n\nOperators describe inference providers at this high level (backend-agnostic\nvocabulary) instead of authoring raw OGX provider blocks. The\nsynthesizer (`apply_high_level_inference`) expands each entry into a Llama\nStack `providers.inference` entry, mapping `type` to a `provider_type` and\nemitting `${env.}` references for secrets (never literal values).\n\nAttributes:\n type: Canonical provider identifier. Vendor-neutral so it survives a\n future backend change; each backend-specific synthesizer maps it to\n its own provider vocabulary.\n id: Optional identifier emitted as the OGX provider_id. When\n omitted, synthesized as type with underscores hyphenated. If set,\n must be non-empty after stripping whitespace and may contain only\n lowercase letters, digits, underscores, and hyphens.\n api_key_env: Name of the environment variable holding the provider API\n key. Emitted verbatim as `${env.}` so the secret never lands\n on disk resolved.\n allowed_models: Optional allow-list of model identifiers passed through\n to the synthesized provider config.\n extra: Additional provider-config keys merged verbatim into the\n synthesized provider's `config` block — an escape hatch for\n provider-specific knobs not modeled here.", + "description": "A high-level inference provider entry for unified-mode synthesis.\n\nOperators describe inference providers at this high level (backend-agnostic\nvocabulary) instead of authoring raw OGX provider blocks. The\nsynthesizer (`apply_high_level_inference`) expands each entry into an OGX\n`providers.inference` entry, mapping `type` to a `provider_type` and\nemitting `${env.}` references for secrets (never literal values).\n\nAttributes:\n type: Canonical provider identifier. Vendor-neutral so it survives a\n future backend change; each backend-specific synthesizer maps it to\n its own provider vocabulary.\n id: Optional identifier emitted as the OGX provider_id. When\n omitted, synthesized as type with underscores hyphenated. If set,\n must be non-empty after stripping whitespace and may contain only\n lowercase letters, digits, underscores, and hyphens.\n api_key_env: Name of the environment variable holding the provider API\n key. Emitted verbatim as `${env.}` so the secret never lands\n on disk resolved.\n allowed_models: Optional allow-list of model identifiers passed through\n to the synthesized provider config.\n extra: Additional provider-config keys merged verbatim into the\n synthesized provider's `config` block — an escape hatch for\n provider-specific knobs not modeled here.", "properties": { "type": { - "description": "Canonical, backend-agnostic provider identifier mapped to a OGX provider_type by the synthesizer.", + "description": "Canonical, backend-agnostic provider identifier mapped to an OGX provider_type by the synthesizer.", "enum": [ "openai", "ollama", @@ -2249,7 +2426,7 @@ }, "UnifiedOgxConfig": { "additionalProperties": false, - "description": "Backend-specific knobs for unified-mode OGX synthesis.\n\nPer Decision S5 of the design spike, backend-agnostic high-level sections\n(inference, ...) live at the configuration root, not here. This block holds\nonly the OGX-specific synthesis controls: which baseline to start\nfrom, an optional profile file, and a raw native_override escape hatch.\n\nAttributes:\n baseline: Synthesis starting point. \"default\" begins from LCORE's\n built-in baseline (src/data/default_run.yaml); \"empty\" begins from\n an empty dict (used by the migration tool for an exact round-trip).\n Ignored when `profile` is set.\n profile: Optional path to a user-authored run.yaml-shaped file used as\n the synthesis baseline. Relative paths resolve against the directory\n of the loaded lightspeed-stack.yaml.\n native_override: Raw OGX schema deep-merged last (maps merge\n recursively, lists and scalars replace). The escape hatch for\n anything the high-level sections do not express.", + "description": "Backend-specific knobs for unified-mode OGX synthesis.\n\nPer Decision S5 of the design spike, backend-agnostic high-level sections\n(inference, ...) live at the configuration root, not here. This block holds\nonly the OGX-specific synthesis controls: which baseline to start\nfrom, an optional profile file, and a raw native_override escape hatch.\n\nAttributes:\n baseline: Synthesis starting point. \"default\" begins from LCORE's\n built-in baseline (src/data/default_run.yaml) including the\n conditional OpenAI inference provider. \"byo-llm\" begins from the\n same file with that OpenAI row removed. \"empty\" begins from an\n empty dict (used by the migration tool for an exact round-trip).\n Ignored when `profile` is set.\n profile: Optional path to a user-authored run.yaml-shaped file used as\n the synthesis baseline. Relative paths resolve against the directory\n of the loaded lightspeed-stack.yaml.\n native_override: Raw OGX schema deep-merged last (maps merge\n recursively, lists and scalars replace). The escape hatch for\n anything the high-level sections do not express.", "properties": { "baseline": { "default": "default", diff --git a/docs/user_doc/config.md b/docs/user_doc/config.md index e53138fde..eca3a16c0 100644 --- a/docs/user_doc/config.md +++ b/docs/user_doc/config.md @@ -221,34 +221,34 @@ Attributes: Global service configuration. -| Field | Type | Description | -|------------------------|--------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| name | string | Name of the service. That value will be used in REST API endpoints. | -| config_format_version | string | Optional explicit marker of the configuration format. When set, it must agree with the shape detected from the configuration body: 'unified' requires a synthesis input (a non-empty inference.providers, a non-empty vector_store.providers, or a ogx.config block), 'legacy' requires no synthesis input. Reserved as the lever for a future breaking change of the unified schema (R11). | -| service | | This section contains Lightspeed Core Stack service configuration. | -| ogx | | This section contains OGX configuration. Lightspeed Core Stack service can call OGX in library mode or in server mode. | -| user_data_collection | | This section contains configuration for subsystem that collects user data(transcription history and feedbacks). | -| database | | Configuration for database to store conversation IDs and other runtime data | -| mcp_servers | array | MCP (Model Context Protocol) servers provide tools and capabilities to the AI agents. These are configured in this section. Only MCP servers defined in the lightspeed-stack.yaml configuration are available to the agents. Tools configured in the OGX run.yaml are not accessible to lightspeed-core agents. | -| authentication | | Authentication configuration | -| authorization | | Lightspeed Core Stack implements a modular authentication and authorization system with multiple authentication methods. Authorization is configurable through role-based access control. Authentication is handled through selectable modules configured via the module field in the authentication configuration. | -| customization | | It is possible to customize Lightspeed Core Stack via this section. System prompt can be customized and also different parts of the service can be replaced by custom Python modules. | -| inference | | One LLM provider and one its model might be selected as default ones. When no provider+model pair is specified in REST API calls (query endpoints), the default provider and model are used. | -| conversation_cache | | | -| compaction | | Controls when conversation history is summarized to keep the model's input below the context window limit. Disabled by default — when disabled, requests that exceed the window continue to surface as HTTP 413. | -| approvals | | Settings for human-in-the-loop approval of MCP tool invocations | -| vector_store | | Dynamic vector-store provider capacity for runtime POST /v1/vector-stores creates. Not the same as rag.byok.stores (static registered corpora). When providers is non-empty, default_provider is required and must match one of providers[].id. Applied in unified synthesis only. | -| a2a_state | | Configuration for A2A protocol persistent state storage. | -| quota_handlers | | Quota handlers configuration | -| azure_entra_id | | | -| rlsapi_v1 | | Configuration for the rlsapi v1 /infer endpoint used by the RHEL Lightspeed Command Line Assistant (CLA). | -| splunk | | Splunk HEC configuration for sending telemetry events. | -| observability | | OpenTelemetry and observability configuration collected from OTEL_* environment variables. | -| deployment_environment | string | Deployment environment name (e.g., 'development', 'staging', 'production'). Used in telemetry events. | -| rag | | Unified RAG configuration: BYOK stores, OKP provider, and retrieval strategies (inline and tool-based). | -| skills | | Agent skills configuration. Specifies paths to skill directories. | -| saved_prompts | | Configuration for saved prompts feature limits including maximum prompts per user, display name length, and content length. | -| shields | array | List of pydantic-ai-lightspeed agent guardrail shields (question validity and PII redaction). Each entry has a unique 'name', a 'provider_id' ('question_validity' or 'redaction'), and a type-specific 'config'. | +| Field | Type | Description | +|------------------------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| name | string | Name of the service. That value will be used in REST API endpoints. | +| config_format_version | string | Optional explicit marker of the configuration format. When set, it must agree with the shape detected from the configuration body: 'unified' requires a synthesis input (a non-empty inference.providers, a non-empty vector_store.providers, or an ogx.config block), 'legacy' requires no synthesis input. Reserved as the lever for a future breaking change of the unified schema (R11). | +| service | | This section contains Lightspeed Core Stack service configuration. | +| ogx | | This section contains OGX configuration. Lightspeed Core Stack service can call OGX in library mode or in server mode. | +| user_data_collection | | This section contains configuration for subsystem that collects user data(transcription history and feedbacks). | +| database | | Configuration for database to store conversation IDs and other runtime data | +| mcp_servers | array | MCP (Model Context Protocol) servers provide tools and capabilities to the AI agents. These are configured in this section. Only MCP servers defined in the lightspeed-stack.yaml configuration are available to the agents. Tools configured in the OGX run.yaml are not accessible to lightspeed-core agents. | +| authentication | | Authentication configuration | +| authorization | | Lightspeed Core Stack implements a modular authentication and authorization system with multiple authentication methods. Authorization is configurable through role-based access control. Authentication is handled through selectable modules configured via the module field in the authentication configuration. | +| customization | | It is possible to customize Lightspeed Core Stack via this section. System prompt can be customized and also different parts of the service can be replaced by custom Python modules. | +| inference | | One LLM provider and one its model might be selected as default ones. When no provider+model pair is specified in REST API calls (query endpoints), the default provider and model are used. | +| conversation_cache | | | +| compaction | | Controls when conversation history is summarized to keep the model's input below the context window limit. Disabled by default — when disabled, requests that exceed the window continue to surface as HTTP 413. | +| approvals | | Settings for human-in-the-loop approval of MCP tool invocations | +| vector_store | | Dynamic vector-store provider capacity for runtime POST /v1/vector-stores creates. Not the same as rag.byok.stores (static registered corpora). When providers is non-empty, default_provider is required and must match one of providers[].id. Applied in unified synthesis only. | +| a2a_state | | Configuration for A2A protocol persistent state storage. | +| quota_handlers | | Quota handlers configuration | +| azure_entra_id | | | +| rlsapi_v1 | | Configuration for the rlsapi v1 /infer endpoint used by the RHEL Lightspeed Command Line Assistant (CLA). | +| splunk | | Splunk HEC configuration for sending telemetry events. | +| observability | | OpenTelemetry and observability configuration collected from OTEL_* environment variables. | +| deployment_environment | string | Deployment environment name (e.g., 'development', 'staging', 'production'). Used in telemetry events. | +| rag | | Unified RAG configuration: BYOK stores, OKP provider, and retrieval strategies (inline and tool-based). | +| skills | | Agent skills configuration. Specifies paths to skill directories. | +| saved_prompts | | Configuration for saved prompts feature limits including maximum prompts per user, display name length, and content length. | +| shields | array | List of pydantic-ai-lightspeed agent guardrail shields (question validity and PII redaction). Each entry has a unique 'name', a 'provider_id' ('question_validity' or 'redaction'), and a type-specific 'config'. | ## ConversationHistoryConfiguration @@ -313,13 +313,13 @@ Database configuration. Dynamic FAISS vector-store provider (runtime create capacity). -| Field | Type | Description | -|---------------------|---------|-------------------------------------------------------------------------------------------------------| +| Field | Type | Description | +|---------------------|---------|-----------------------------------------------------------------------------------------------| | id | string | OGX vector_io provider_id. Surrounding whitespace is stripped before validation and emission. | -| embedding_model | string | Embedding model identification used for stores created against this provider. | -| embedding_dimension | integer | Dimensionality of embedding vectors for this provider. | -| type | string | Product type for this dynamic vector-store provider. | -| config | | FAISS storage settings for this provider. | +| embedding_model | string | Embedding model identification used for stores created against this provider. | +| embedding_dimension | integer | Dimensionality of embedding vectors for this provider. | +| type | string | Product type for this dynamic vector-store provider. | +| config | | FAISS storage settings for this provider. | ## FaissVectorStoreProviderConfig @@ -333,6 +333,40 @@ Storage config for a FAISS dynamic vector-store provider. | path | string | On-disk FAISS/SQLite path for this provider. | +## GraniteGuardianConfig + + +Configuration for the Granite Guardian moderation guardrail. + + +| Field | Type | Description | +|-------------|---------|----------------------------------------------| +| url | string | The model_id to use for the guard | +| api_key | string | API key for the inference | +| max_retries | integer | Maximun number of retires | +| timeout | integer | Request timeout in seconds | +| verify_ssl | | SSL certificate verification | +| risks | array | Risks to be considered while applying this guradrail | + + +## GraniteGuardianShieldConfiguration + + +Configuration for a named Granite Guardian guardrail shield. + +Attributes: + name: Unique, user-facing name identifying this shield instance. + provider_id: Discriminator identifying this as a granite-guardian shield. + config: Granite-guardian-specific configuration. + + +| Field | Type | Description | +|-------------|--------|--------------------------------------------------------------| +| name | string | Unique, user-facing name identifying this shield instance. | +| provider_id | string | Discriminator identifying this as a granite-guardian shield. | +| config | | Granite-guardian-specific configuration for this shield | + + ## InMemoryCacheConfig @@ -350,14 +384,14 @@ In-memory cache configuration. Inference configuration. -| Field | Type | Description | -|------------------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| default_model | string | Identification of default model used when no other model is specified. | -| default_provider | string | Identification of default provider used when no other model is specified. | -| context_windows | object | Map of fully-qualified model identifier (e.g., "openai/gpt-4o-mini") to context window size in tokens. Used by the conversation compaction trigger to decide when older turns must be summarized before the input exceeds the window. Models absent from this map have no registered window — callers fall back to their own default or skip the token-based trigger. | +| Field | Type | Description | +|------------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| default_model | string | Identification of default model used when no other model is specified. | +| default_provider | string | Identification of default provider used when no other model is specified. | +| context_windows | object | Map of fully-qualified model identifier (e.g., "openai/gpt-4o-mini") to context window size in tokens. Used by the conversation compaction trigger to decide when older turns must be summarized before the input exceeds the window. Models absent from this map have no registered window — callers fall back to their own default or skip the token-based trigger. | | providers | array | Unified-mode synthesis input (Decision S5): a high-level, backend-agnostic list of inference providers the synthesizer expands into OGX provider entries. Lives at the configuration root so it survives a future backend change. A non-empty list signals unified mode. Empty (the default) leaves legacy/remote modes unaffected. The sibling default_model / default_provider keep their query-time routing meaning and are independent of this list. | -| max_infer_iters | integer | Server-side default for the maximum number of inference iterations a model can perform in a single request. Prevents small models from looping indefinitely on tool calls. Per-request values take precedence over this default. Set to None to disable the limit. | -| max_tool_calls | integer | Server-side default for the maximum number of tool calls allowed in a single response. Prevents small models from exhausting the context window with repeated tool calls. Per-request values take precedence over this default. Set to None to disable the limit. | +| max_infer_iters | integer | Server-side default for the maximum number of inference iterations a model can perform in a single request. Prevents small models from looping indefinitely on tool calls. Per-request values take precedence over this default. Set to None to disable the limit. | +| max_tool_calls | integer | Server-side default for the maximum number of tool calls allowed in a single response. Prevents small models from exhausting the context window with repeated tool calls. Per-request values take precedence over this default. Set to None to disable the limit. | ## JsonPathOperator @@ -432,36 +466,6 @@ Rule for extracting roles from JWT claims. | roles | array | Roles to be assigned if the rule matches | -## OgxConfiguration - - -OGX configuration. - -OGX is a comprehensive system that provides a uniform set of tools -for building, scaling, and deploying generative AI applications, enabling -developers to create, integrate, and orchestrate multiple AI services and -capabilities into an adaptable setup. - -Useful resources: - - - [OGX](https://ogx-ai.github.io/) - - [Python OGX client](https://github.com/ogx-ai/ogx-client-python) - - [Build AI Applications with OGX](https://ogx-ai.github.io/docs/building_applications) - - -| Field | Type | Description | -|----------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| url | string | URL to OGX service; used when library mode is disabled. Must be a valid HTTP or HTTPS URL. | -| api_key | string | API key to access OGX service | -| use_as_library_client | boolean | When set to true OGX will be used in library mode, not in server mode (default) | -| library_client_config_path | string | Path to configuration file used when OGX is run in library mode. DEPRECATED legacy two-file setup: logs a startup warning since 0.6 and is removed in 0.8 — use unified mode instead (the config block below, and/or the root-level inference.providers section); migrate with lightspeed-stack --migrate-config. | -| timeout | integer | Timeout in seconds for requests to OGX service. Default is 180 seconds (3 minutes) to accommodate long-running RAG queries. | -| max_retries | integer | Maximum number of connection attempts before giving up. Used on startup to connect to OGX and retrieve its version. Connection attempts are retried with a fixed delay to handle the case where OGX is still starting up (e.g., when running as a sidecar in the same pod). | -| retry_delay | integer | Delay in seconds between retry attempts. Used on startup to connect to OGX and retrieve its version. Connection attempts are retried with a fixed delay to handle the case where OGX is still starting up (e.g., when running as a sidecar in the same pod). | -| allow_degraded_mode | boolean | If enabled, Lightspeed Core can be started even when OGX is not accessible (valid for server mode only) | -| config | | Backend-specific knobs for unified mode, where LCORE synthesizes the OGX run.yaml instead of reading an external file. Holds the baseline selector, an optional profile path, and a raw native_override escape hatch. Backend-agnostic high-level sections (e.g. inference.providers) live at the configuration root, not here. Mutually exclusive with library_client_config_path; that cross-field check lives on the root Configuration model. When set in library mode, library_client_config_path is not required. | - - ## ModelContextProtocolServer @@ -488,7 +492,7 @@ Useful resources: | authorization_headers | object | Headers to send to the MCP server. The map contains the header name and the path to a file containing the header value (secret). There are 3 special cases: 1. Usage of the kubernetes token in the header. To specify this use a string 'kubernetes' instead of the file path. 2. Usage of the client-provided token in the header. To specify this use a string 'client' instead of the file path. 3. Usage of the oauth token in the header. To specify this use a string 'oauth' instead of the file path. | | headers | array | List of HTTP header names to automatically forward from the incoming request to this MCP server. Headers listed here are extracted from the original client request and included when calling the MCP server. This is useful when infrastructure components (e.g. API gateways) inject headers that MCP servers need, such as x-rh-identity in HCC. Header matching is case-insensitive. These headers are additive with authorization_headers and MCP-HEADERS. | | require_approval | | When to require human approval for tool invocations. 'always' requires approval for all tools, 'never' auto-approves, or use ApprovalFilter for granular control. | -| timeout | integer | Timeout in seconds for requests to the MCP server. If not specified, the default timeout from OGX will be used. Note: This field is reserved for future use when OGX adds timeout support. | +| timeout | integer | Timeout in seconds for requests to the MCP server. If not specified, the default timeout from OGX will be used. Note: This field is reserved for future use when OGX adds timeout support. | ## ObservabilityConfiguration @@ -508,6 +512,36 @@ Attributes: | otel | object | Active OpenTelemetry configuration from OTEL_* environment variables | +## OgxConfiguration + + +OGX configuration. + +OGX is a comprehensive system that provides a uniform set of tools +for building, scaling, and deploying generative AI applications, enabling +developers to create, integrate, and orchestrate multiple AI services and +capabilities into an adaptable setup. + +Useful resources: + + - [OGX](https://ogx-ai.github.io/) + - [Python OGX client](https://github.com/ogx-ai/ogx-client-python) + - [Build AI Applications with OGX](https://ogx-ai.github.io/docs/building_applications) + + +| Field | Type | Description | +|----------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| url | string | URL to OGX service; used when library mode is disabled. Must be a valid HTTP or HTTPS URL. | +| api_key | string | API key to access OGX service | +| use_as_library_client | boolean | When set to true OGX will be used in library mode, not in server mode (default) | +| library_client_config_path | string | Path to configuration file used when OGX is run in library mode. DEPRECATED legacy two-file setup: logs a startup warning since 0.6 and is removed in 0.8 — use unified mode instead (the config block below, and/or the root-level inference.providers section); migrate with lightspeed-stack --migrate-config. | +| timeout | integer | Timeout in seconds for requests to OGX service. Default is 180 seconds (3 minutes) to accommodate long-running RAG queries. | +| max_retries | integer | Maximum number of connection attempts before giving up. Used on startup to connect to OGX and retrieve its version. Connection attempts are retried with a fixed delay to handle the case where OGX is still starting up (e.g., when running as a sidecar in the same pod). | +| retry_delay | integer | Delay in seconds between retry attempts. Used on startup to connect to OGX and retrieve its version. Connection attempts are retried with a fixed delay to handle the case where OGX is still starting up (e.g., when running as a sidecar in the same pod). | +| allow_degraded_mode | boolean | If enabled, Lightspeed Core can be started even when OGX is not accessible (valid for server mode only) | +| config | | Backend-specific knobs for unified mode, where LCORE synthesizes the OGX run.yaml instead of reading an external file. Holds the baseline selector, an optional profile path, and a raw native_override escape hatch. Backend-agnostic high-level sections (e.g. inference.providers) live at the configuration root, not here. Mutually exclusive with library_client_config_path; that cross-field check lives on the root Configuration model. When set in library mode, library_client_config_path is not required. | + + ## OkpConfiguration @@ -533,13 +567,13 @@ or ``rag.retrieval.tool.sources``. Dynamic pgvector vector-store provider (runtime create capacity). -| Field | Type | Description | -|---------------------|---------|-------------------------------------------------------------------------------------------------------| +| Field | Type | Description | +|---------------------|---------|-----------------------------------------------------------------------------------------------| | id | string | OGX vector_io provider_id. Surrounding whitespace is stripped before validation and emission. | -| embedding_model | string | Embedding model identification used for stores created against this provider. | -| embedding_dimension | integer | Dimensionality of embedding vectors for this provider. | -| type | string | Product type for this dynamic vector-store provider. | -| config | | pgvector connection settings for this provider. | +| embedding_model | string | Embedding model identification used for stores created against this provider. | +| embedding_dimension | integer | Dimensionality of embedding vectors for this provider. | +| type | string | Product type for this dynamic vector-store provider. | +| config | | pgvector connection settings for this provider. | ## PgvectorVectorStoreProviderConfig @@ -725,7 +759,7 @@ BYOK (Bring Your Own Knowledge) RAG store configuration. | embedding_dimension | integer | Dimensionality of embedding vectors. | | vector_db_id | string | Vector database identification. | | db_path | string | Path to RAG database. Required for faiss backend. | -| score_multiplier | number | Multiplier applied to relevance scores from this vector store. Used to weight results when querying multiple knowledge sources. Values > 1 boost this store's results; values < 1 reduce them. | +| score_multiplier | number | Multiplier applied to relevance scores from this vector store. Used to weight results when querying multiple knowledge sources. Values > 1 boost this store's results; values < 1 reduce them. | | relevance_cutoff_score | number | Minimum raw similarity score to consider a result relevant. Results with a similarity score below this threshold are not returned. | | host | string | PostgreSQL host for pgvector backend. Defaults to ${env.POSTGRES_HOST} when backend is pgvector. | | port | | PostgreSQL port for pgvector backend. Defaults to ${env.POSTGRES_PORT} when backend is pgvector. | @@ -829,6 +863,44 @@ Configuration for a single retrieval strategy (inline or tool). | reranker | | Neural reranking of RAG chunks using cross-encoder. Only applicable to inline retrieval. | +## RiskDefinition + + +Definition for a custom risk category. + +Custom risks allow applications to add use-case-specific safety checks +beyond the standard harm, jailbreak, leetspeak, amnesia, and +history_politics checks. +Example: + liability_risk = RiskDefinition( + name="liability", + description="Content requesting legal, medical, or financial advice", + threshold=0.55, + points=["input"], + ) + pii_risk = RiskDefinition( + name="pii_request", + description="User is asking the AI to reveal personal information", + threshold=0.50, + points=["input", "tool"], + ) +Note: + To enable think mode (detailed reasoning) for a risk, add the risk name + to the `thinking_enabled` list in `ModerationConfig`. Do not set + `enable_thinking` directly - it is managed internally. + + +| Field | Type | Description | +|-------------------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------| +| name | string | Unique identifier for this risk (e.g., 'liability', 'competitor_mention') | +| description | string | Risk definition text passed to Granite Guardian as custom_criteria | +| threshold | number | Score threshold for flagging (lower = more sensitive) | +| enabled | boolean | Whether to run this check | +| enable_thinking | boolean | Internal field - set via ModerationConfig.thinking_enabled list, not directly. When True, Granite Guardian provides detailed reasoning before scoring. | +| points | array | Where this risk is evaluated: `input` (user message), `output` (model response), or `tool` (tool/MCP content). | +| violation_message | string | Message to be displayed when this risk is violated | + + ## RlsapiV1Configuration @@ -889,18 +961,21 @@ and specify the number of Uvicorn workers. When more workers are specified, the service can handle requests concurrently. -| Field | Type | Description | -|--------------|---------|------------------------------------------------------------------------| -| host | string | Service hostname | -| port | integer | Service port | -| base_url | string | Externally reachable base URL for the service; needed for A2A support. | -| auth_enabled | boolean | Enables the authentication subsystem | -| workers | integer | Number of Uvicorn worker processes to start | -| color_log | boolean | Enables colorized logging | -| access_log | boolean | Enables logging of all access information | -| tls_config | | Transport Layer Security configuration for HTTPS support | -| root_path | string | ASGI root path for serving behind a reverse proxy on a subpath | -| cors | | Cross-Origin Resource Sharing configuration for cross-domain requests | +| Field | Type | Description | +|---------------------------------------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| host | string | Service hostname | +| port | integer | Service port | +| base_url | string | Externally reachable base URL for the service; needed for A2A support. | +| auth_enabled | boolean | Enables the authentication subsystem | +| workers | integer | Number of Uvicorn worker processes to start | +| max_concurrent_file_uploads | integer | Maximum number of file uploads (POST /v1/files) processed concurrently per worker. Each in-flight upload can hold up to the configured maximum file size in memory, so this bounds worst-case memory usage from concurrent uploads. Additional uploads are rejected with 429 until a slot frees up. | +| max_concurrent_vector_store_attaches | integer | Maximum number of vector store file attachments (POST /v1/vector-stores/{id}/files) processed concurrently per worker. Each in-flight attachment re-reads and chunks the source file, so this bounds worst-case memory usage independently of max_concurrent_file_uploads. Additional attachments are rejected with 429 until a slot frees up. | +| delete_file_after_vector_store_attach | boolean | When true, deletes a file (POST /v1/files) once it has been successfully attached to a vector store, since the vector store keeps its own chunked/embedded copy of the content. Defaults to false to match the OpenAI Files API, where a file remains reusable across multiple vector stores until the caller explicitly deletes it - enabling this makes attached files single-use: re-attaching the same file_id to another vector store will fail once it has been deleted. | +| color_log | boolean | Enables colorized logging | +| access_log | boolean | Enables logging of all access information | +| tls_config | | Transport Layer Security configuration for HTTPS support | +| root_path | string | ASGI root path for serving behind a reverse proxy on a subpath | +| cors | | Cross-Origin Resource Sharing configuration for cross-domain requests | ## SkillsConfiguration @@ -1027,13 +1102,13 @@ Attributes: provider-specific knobs not modeled here. -| Field | Type | Description | -|----------------|--------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| type | string | Canonical, backend-agnostic provider identifier mapped to a OGX provider_type by the synthesizer. | +| Field | Type | Description | +|----------------|--------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| type | string | Canonical, backend-agnostic provider identifier mapped to an OGX provider_type by the synthesizer. | | id | string | Optional identifier emitted as the OGX provider_id. When omitted, synthesized as type with underscores hyphenated. If set, must be non-empty after stripping whitespace and may contain only lowercase letters, digits, underscores, and hyphens. | -| api_key_env | string | Name of the environment variable holding the provider API key. Emitted as a ${env.} reference so the secret is never written to disk in resolved form. | -| allowed_models | array | Optional allow-list of model identifiers for this provider. | -| extra | object | Additional provider-config keys merged verbatim into the synthesized provider's config block. | +| api_key_env | string | Name of the environment variable holding the provider API key. Emitted as a ${env.} reference so the secret is never written to disk in resolved form. | +| allowed_models | array | Optional allow-list of model identifiers for this provider. | +| extra | object | Additional provider-config keys merged verbatim into the synthesized provider's config block. | ## UnifiedOgxConfig @@ -1046,13 +1121,6 @@ Per Decision S5 of the design spike, backend-agnostic high-level sections only the OGX-specific synthesis controls: which baseline to start from, an optional profile file, and a raw native_override escape hatch. -During synthesis from the default baseline or a profile, LCORE ensures the -OGX MCP tool_runtime provider (`provider_id: model-context-protocol`, -`provider_type: remote::model-context-protocol`) is present so static -`mcp_servers` and dynamic MCP registration work. That ensure is skipped when -`baseline: empty` (migration / blank-slate); supply MCP via `native_override` -in that case. - Attributes: baseline: Synthesis starting point. "default" begins from LCORE's built-in baseline (src/data/default_run.yaml) including the @@ -1068,11 +1136,11 @@ Attributes: anything the high-level sections do not express. -| Field | Type | Description | -|-----------------|--------|----------------------------------------------------------------------------------------------------------------------------| +| Field | Type | Description | +|-----------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | baseline | string | Synthesis starting point: 'default' uses LCORE's built-in baseline including the conditional OpenAI provider, 'byo-llm' uses the same baseline without that OpenAI row, 'empty' starts from {}. Ignored when 'profile' is set. | -| profile | string | Path to a run.yaml-shaped baseline file. Relative paths resolve against the directory of the loaded lightspeed-stack.yaml. | -| native_override | object | Raw OGX schema deep-merged last (maps merge recursively; lists and scalars replace). | +| profile | string | Path to a run.yaml-shaped baseline file. Relative paths resolve against the directory of the loaded lightspeed-stack.yaml. | +| native_override | object | Raw OGX schema deep-merged last (maps merge recursively; lists and scalars replace). | ## UserDataCollection @@ -1107,7 +1175,7 @@ Attributes: registered corpora). -| Field | Type | Description | -|------------------|--------|-------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Field | Type | Description | +|------------------|--------|-----------------------------------------------------------------------------------------------------------------------------------------------------| | default_provider | string | Provider id used for vector_stores.default_* in the synthesized OGX config. Required when providers is non-empty; must match one of providers[].id. | -| providers | array | Dynamic vector-store provider capacity for runtime POST /v1/vector-stores creates. Not the same as rag.byok.stores (static registered corpora). | +| providers | array | Dynamic vector-store provider capacity for runtime POST /v1/vector-stores creates. Not the same as rag.byok.stores (static registered corpora). | From 83901b38753348e211d58a955a4adfe58296adc7 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Sun, 13 Sep 2026 09:53:16 +0200 Subject: [PATCH 085/120] Moved MCP utils into its own module --- src/utils/{ => mcp}/mcp_auth_headers.py | 0 src/utils/{ => mcp}/mcp_headers.py | 0 src/utils/{ => mcp}/mcp_oauth_probe.py | 2 +- src/utils/{ => mcp}/mcp_tools.py | 0 4 files changed, 1 insertion(+), 1 deletion(-) rename src/utils/{ => mcp}/mcp_auth_headers.py (100%) rename src/utils/{ => mcp}/mcp_headers.py (100%) rename src/utils/{ => mcp}/mcp_oauth_probe.py (98%) rename src/utils/{ => mcp}/mcp_tools.py (100%) diff --git a/src/utils/mcp_auth_headers.py b/src/utils/mcp/mcp_auth_headers.py similarity index 100% rename from src/utils/mcp_auth_headers.py rename to src/utils/mcp/mcp_auth_headers.py diff --git a/src/utils/mcp_headers.py b/src/utils/mcp/mcp_headers.py similarity index 100% rename from src/utils/mcp_headers.py rename to src/utils/mcp/mcp_headers.py diff --git a/src/utils/mcp_oauth_probe.py b/src/utils/mcp/mcp_oauth_probe.py similarity index 98% rename from src/utils/mcp_oauth_probe.py rename to src/utils/mcp/mcp_oauth_probe.py index 570e968eb..17c3d265d 100644 --- a/src/utils/mcp_oauth_probe.py +++ b/src/utils/mcp/mcp_oauth_probe.py @@ -15,7 +15,7 @@ from configuration import AppConfig from log import get_logger from models.api.responses.error import UnauthorizedResponse -from utils.mcp_headers import McpHeaders, build_mcp_headers +from utils.mcp.mcp_headers import McpHeaders, build_mcp_headers logger = get_logger(__name__) diff --git a/src/utils/mcp_tools.py b/src/utils/mcp/mcp_tools.py similarity index 100% rename from src/utils/mcp_tools.py rename to src/utils/mcp/mcp_tools.py From 762877edcf6e54e80c8635758d5b54e361d3239a Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Sun, 13 Sep 2026 09:53:34 +0200 Subject: [PATCH 086/120] Move unit tests as well --- tests/unit/utils/{ => mcp}/test_mcp_auth_headers.py | 2 +- tests/unit/utils/{ => mcp}/test_mcp_headers.py | 4 ++-- tests/unit/utils/{ => mcp}/test_mcp_tools.py | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) rename tests/unit/utils/{ => mcp}/test_mcp_auth_headers.py (98%) rename tests/unit/utils/{ => mcp}/test_mcp_headers.py (99%) rename tests/unit/utils/{ => mcp}/test_mcp_tools.py (88%) diff --git a/tests/unit/utils/test_mcp_auth_headers.py b/tests/unit/utils/mcp/test_mcp_auth_headers.py similarity index 98% rename from tests/unit/utils/test_mcp_auth_headers.py rename to tests/unit/utils/mcp/test_mcp_auth_headers.py index 2404d282f..6898ec673 100644 --- a/tests/unit/utils/test_mcp_auth_headers.py +++ b/tests/unit/utils/mcp/test_mcp_auth_headers.py @@ -2,7 +2,7 @@ from pathlib import Path -from utils.mcp_auth_headers import resolve_authorization_headers +from utils.mcp.mcp_auth_headers import resolve_authorization_headers def test_resolve_authorization_headers_empty() -> None: diff --git a/tests/unit/utils/test_mcp_headers.py b/tests/unit/utils/mcp/test_mcp_headers.py similarity index 99% rename from tests/unit/utils/test_mcp_headers.py rename to tests/unit/utils/mcp/test_mcp_headers.py index 0e583b7f3..8bc4b4449 100644 --- a/tests/unit/utils/test_mcp_headers.py +++ b/tests/unit/utils/mcp/test_mcp_headers.py @@ -8,8 +8,8 @@ import constants from models.config import ModelContextProtocolServer -from utils import mcp_headers -from utils.mcp_headers import ( +from utils.mcp import mcp_headers +from utils.mcp.mcp_headers import ( build_server_headers, extract_propagated_headers, find_unresolved_auth_headers, diff --git a/tests/unit/utils/test_mcp_tools.py b/tests/unit/utils/mcp/test_mcp_tools.py similarity index 88% rename from tests/unit/utils/test_mcp_tools.py rename to tests/unit/utils/mcp/test_mcp_tools.py index 491784e08..bbcac54f2 100644 --- a/tests/unit/utils/test_mcp_tools.py +++ b/tests/unit/utils/mcp/test_mcp_tools.py @@ -4,7 +4,7 @@ import pytest from pytest_mock import MockerFixture -from utils.mcp_tools import _MCP_HTTP_TIMEOUT, list_mcp_tools +from utils.mcp.mcp_tools import _MCP_HTTP_TIMEOUT, list_mcp_tools @pytest.mark.asyncio @@ -17,16 +17,16 @@ async def test_list_mcp_tools_forwards_headers_to_transport( mock_http_client.__aexit__ = mocker.AsyncMock(return_value=None) mock_async_client = mocker.patch( - "utils.mcp_tools.httpx.AsyncClient", + "utils.mcp.mcp_tools.httpx.AsyncClient", return_value=mock_http_client, ) - mock_streamable = mocker.patch("utils.mcp_tools.streamable_http_client") + mock_streamable = mocker.patch("utils.mcp.mcp_tools.streamable_http_client") mock_streamable.return_value.__aenter__ = mocker.AsyncMock( return_value=(mocker.Mock(), mocker.Mock(), mocker.Mock()) ) mock_streamable.return_value.__aexit__ = mocker.AsyncMock(return_value=None) mocker.patch( - "utils.mcp_tools._list_tools_from_session", + "utils.mcp.mcp_tools._list_tools_from_session", new=mocker.AsyncMock(return_value=[]), ) @@ -54,7 +54,7 @@ async def test_list_mcp_tools_returns_empty_list_when_all_transports_fail( ) -> None: """Skip unavailable MCP servers by returning an empty tool list.""" mocker.patch( - "utils.mcp_tools._MCP_TRANSPORTS", + "utils.mcp.mcp_tools._MCP_TRANSPORTS", ( ( "streamable HTTP", @@ -82,7 +82,7 @@ async def test_list_mcp_tools_returns_empty_list_on_http_error( response=response, ) mocker.patch( - "utils.mcp_tools._MCP_TRANSPORTS", + "utils.mcp.mcp_tools._MCP_TRANSPORTS", ( ("streamable HTTP", mocker.AsyncMock(side_effect=http_error)), ("SSE", mocker.AsyncMock(side_effect=http_error)), From 13501e75a25e1db49d7cdad049bf25e6c35f1642 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Sun, 13 Sep 2026 09:53:43 +0200 Subject: [PATCH 087/120] Fixed imports --- src/app/endpoints/a2a.py | 2 +- src/app/endpoints/query.py | 4 ++-- src/app/endpoints/responses.py | 4 ++-- src/app/endpoints/streaming_query.py | 4 ++-- src/app/endpoints/tools.py | 6 +++--- src/models/config.py | 2 +- src/utils/responses.py | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/app/endpoints/a2a.py b/src/app/endpoints/a2a.py index fd7931459..3bd6db1c2 100644 --- a/src/app/endpoints/a2a.py +++ b/src/app/endpoints/a2a.py @@ -68,7 +68,7 @@ apply_compaction_blocking, store_compacted_turn, ) -from utils.mcp_headers import McpHeaders, mcp_headers_dependency +from utils.mcp.mcp_headers import McpHeaders, mcp_headers_dependency from utils.otel_tracing import ( SpanAttributes, SpanEvents, diff --git a/src/app/endpoints/query.py b/src/app/endpoints/query.py index 8b7f12e93..8d5b9c47e 100644 --- a/src/app/endpoints/query.py +++ b/src/app/endpoints/query.py @@ -37,8 +37,8 @@ check_configuration_loaded, validate_and_retrieve_conversation, ) -from utils.mcp_headers import McpHeaders, mcp_headers_dependency -from utils.mcp_oauth_probe import check_mcp_auth +from utils.mcp.mcp_headers import McpHeaders, mcp_headers_dependency +from utils.mcp.mcp_oauth_probe import check_mcp_auth from utils.otel_tracing import ( SpanAttributes, SpanEvents, diff --git a/src/app/endpoints/responses.py b/src/app/endpoints/responses.py index 38110287d..9dc0f833b 100644 --- a/src/app/endpoints/responses.py +++ b/src/app/endpoints/responses.py @@ -71,8 +71,8 @@ check_configuration_loaded, resolve_response_context, ) -from utils.mcp_headers import mcp_headers_dependency -from utils.mcp_oauth_probe import check_mcp_auth +from utils.mcp.mcp_headers import mcp_headers_dependency +from utils.mcp.mcp_oauth_probe import check_mcp_auth from utils.ogx_serialization import dump_ogx_model from utils.otel_tracing import ( SpanAttributes, diff --git a/src/app/endpoints/streaming_query.py b/src/app/endpoints/streaming_query.py index 1c305e257..a681f88fb 100644 --- a/src/app/endpoints/streaming_query.py +++ b/src/app/endpoints/streaming_query.py @@ -60,8 +60,8 @@ check_configuration_loaded, validate_and_retrieve_conversation, ) -from utils.mcp_headers import McpHeaders, mcp_headers_dependency -from utils.mcp_oauth_probe import check_mcp_auth +from utils.mcp.mcp_headers import McpHeaders, mcp_headers_dependency +from utils.mcp.mcp_oauth_probe import check_mcp_auth from utils.otel_tracing import ( SpanAttributes, SpanEvents, diff --git a/src/app/endpoints/tools.py b/src/app/endpoints/tools.py index bd55e99c3..6a22ee471 100644 --- a/src/app/endpoints/tools.py +++ b/src/app/endpoints/tools.py @@ -23,14 +23,14 @@ from models.config import Action, ModelContextProtocolServer from utils.builtin_tools import get_file_search_tools from utils.endpoints import check_configuration_loaded -from utils.mcp_headers import ( +from utils.mcp.mcp_headers import ( McpHeaders, build_mcp_headers, find_unresolved_auth_headers, mcp_headers_dependency, ) -from utils.mcp_oauth_probe import check_mcp_auth -from utils.mcp_tools import list_mcp_tools +from utils.mcp.mcp_oauth_probe import check_mcp_auth +from utils.mcp.mcp_tools import list_mcp_tools from utils.pydantic_ai_helpers import get_agent_capability_tools from utils.tool_formatter import build_catalog_tool diff --git a/src/models/config.py b/src/models/config.py index 66825ed86..bcc2bf8ec 100644 --- a/src/models/config.py +++ b/src/models/config.py @@ -31,7 +31,7 @@ import constants from log import get_logger from utils import checks -from utils.mcp_auth_headers import resolve_authorization_headers +from utils.mcp.mcp_auth_headers import resolve_authorization_headers from utils.types import CompiledPatterns logger = get_logger(__name__) diff --git a/src/utils/responses.py b/src/utils/responses.py index 607002efa..7f856772d 100644 --- a/src/utils/responses.py +++ b/src/utils/responses.py @@ -110,7 +110,7 @@ ) from models.config import RagStore from models.database.conversations import UserConversation -from utils.mcp_headers import ( +from utils.mcp.mcp_headers import ( McpHeaders, build_mcp_headers, find_unresolved_auth_headers, From ff2bc2673d9133463b601a268242f979b14e3edb Mon Sep 17 00:00:00 2001 From: thepetk Date: Sun, 13 Sep 2026 23:37:39 +0200 Subject: [PATCH 088/120] Use rc.path instead of rc.absolute_route.path Signed-off-by: Theofanis Petkos --- src/app/main.py | 7 +--- tests/unit/app/test_main_middleware.py | 47 ++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/app/main.py b/src/app/main.py index bafc74d63..3312a6b73 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -294,12 +294,7 @@ async def send_wrapper(message: Message) -> None: logger.info("Including routers") routers.include_routers(app) -app_routes_paths = [ - rc.original_route.path # pyright: ignore[reportAttributeAccessIssue] - for rc in iter_route_contexts(app.routes) - if hasattr(rc.original_route, "path") - and rc.original_route.path # pyright: ignore[reportAttributeAccessIssue] -] +app_routes_paths = [rc.path for rc in iter_route_contexts(app.routes) if rc.path] logger.debug("Route paths:") for app_routes_path in app_routes_paths: diff --git a/tests/unit/app/test_main_middleware.py b/tests/unit/app/test_main_middleware.py index bd364a7a1..017edf689 100644 --- a/tests/unit/app/test_main_middleware.py +++ b/tests/unit/app/test_main_middleware.py @@ -303,3 +303,50 @@ def test_app_routes_paths_contains_application_routes() -> None: assert "/liveness" in app_routes_paths assert "/readiness" in app_routes_paths assert len(app_routes_paths) > 4 + + +def test_app_routes_paths_contains_versioned_routes() -> None: + """app_routes_paths must include full mount-prefixed paths like /v1/infer. + + iter_route_contexts() exposes two path attributes per route: + - rc.original_route.path — path relative to the sub-router (e.g. /infer) + - rc.path — effective path with the mount prefix (e.g. /v1/infer) + + Using rc.original_route.path silently omits the /v1 mount prefix, so the + middleware path check never matches requests that arrive as /v1/infer and + ls_rest_api_calls_total is never recorded for versioned endpoints. + """ + assert "/v1/infer" in app_routes_paths + + +@pytest.mark.asyncio +async def test_rest_api_metrics_records_when_proxy_strips_prefix( + mocker: MockerFixture, +) -> None: + """Metrics must be recorded when the proxy already stripped root_path. + + When nginx (or any proxy) strips the root_path prefix before forwarding, + lightspeed-stack receives /v1/infer directly — not /api/lightspeed/v1/infer. + The middleware must match /v1/infer against app_routes_paths without any + prefix stripping, and still record the metric. + """ + mocker.patch("app.main.app_routes_paths", ["/v1/infer"]) + mocker.patch.object(fastapi_app, "root_path", "/api/lightspeed") + mock_measure_duration = mocker.patch( + "app.main.recording.measure_response_duration", return_value=nullcontext() + ) + mock_record_call = mocker.patch("app.main.recording.record_rest_api_call") + + async def ok_app(_scope: Scope, _receive: Receive, send: Send) -> None: + await send({"type": "http.response.start", "status": 200, "headers": []}) + await send({"type": "http.response.body", "body": b"ok"}) + + middleware = RestApiMetricsMiddleware(ok_app) + collector = _ResponseCollector() + + # Proxy stripped /api/lightspeed; lightspeed-stack receives /v1/infer directly. + await middleware(_make_scope("/v1/infer"), _noop_receive, collector) + + assert collector.status_code == 200 + mock_measure_duration.assert_called_once_with("/v1/infer") + mock_record_call.assert_called_once_with("/v1/infer", 200) From d285f0817c600d753e2aa45d4017bb4c56c641d8 Mon Sep 17 00:00:00 2001 From: Andrej Simurka Date: Fri, 11 Sep 2026 13:17:20 +0200 Subject: [PATCH 089/120] finish tests/ OGX naming for e2e harness and fixtures --- tests/configuration/benchmarks-postgres.yaml | 6 +- tests/configuration/benchmarks-sqlite.yaml | 6 +- .../lightspeed-stack-proper-name.yaml | 4 +- tests/configuration/lightspeed-stack.yaml | 4 +- tests/configuration/minimal-stack.yaml | 6 +- tests/configuration/run.yaml | 2 +- tests/e2e/configuration/README.md | 5 +- .../lightspeed-stack-shields-empty.yaml | 2 +- ...speed-stack-shields-override-disabled.yaml | 2 +- .../lightspeed-stack-shields.yaml | 4 +- .../lightspeed-stack-authorized.yaml | 4 +- .../lightspeed-stack-negative.yaml | 4 +- .../lightspeed-stack-shields-empty.yaml | 2 +- ...speed-stack-shields-override-disabled.yaml | 2 +- .../server-mode/lightspeed-stack-shields.yaml | 4 +- tests/e2e/features/environment.py | 30 +---- tests/e2e/features/proxy.feature | 6 +- tests/e2e/features/query.feature | 1 - tests/e2e/features/steps/README.md | 4 - tests/e2e/features/steps/shields.py | 35 ------ tests/e2e/features/streaming_query.feature | 3 +- tests/e2e/utils/README.md | 4 - tests/e2e/utils/ogx_config_utils.py | 4 +- tests/e2e/utils/ogx_utils.py | 115 ------------------ tests/e2e/utils/prow_utils.py | 4 +- tests/integration/test_unified_synthesis.py | 4 +- tests/unit/telemetry/conftest.py | 6 +- tests/unit/test_ogx_synthesize.py | 6 +- 28 files changed, 49 insertions(+), 230 deletions(-) delete mode 100644 tests/e2e/features/steps/shields.py delete mode 100644 tests/e2e/utils/ogx_utils.py diff --git a/tests/configuration/benchmarks-postgres.yaml b/tests/configuration/benchmarks-postgres.yaml index ab94a7d59..718fc785c 100644 --- a/tests/configuration/benchmarks-postgres.yaml +++ b/tests/configuration/benchmarks-postgres.yaml @@ -9,12 +9,12 @@ service: access_log: true ogx: # Uses a remote OGX service - # The instance would have already been started with a llama-stack-run.yaml file + # The instance would have already been started with a run.yaml file use_as_library_client: false # Alternative for "as library use" # use_as_library_client: true - # library_client_config_path: - url: http://llama-stack:8321 + # library_client_config_path: + url: http://ogx:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/configuration/benchmarks-sqlite.yaml b/tests/configuration/benchmarks-sqlite.yaml index 63ea9d925..1284c25c1 100644 --- a/tests/configuration/benchmarks-sqlite.yaml +++ b/tests/configuration/benchmarks-sqlite.yaml @@ -9,12 +9,12 @@ service: access_log: true ogx: # Uses a remote OGX service - # The instance would have already been started with a llama-stack-run.yaml file + # The instance would have already been started with a run.yaml file use_as_library_client: false # Alternative for "as library use" # use_as_library_client: true - # library_client_config_path: - url: http://llama-stack:8321 + # library_client_config_path: + url: http://ogx:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/configuration/lightspeed-stack-proper-name.yaml b/tests/configuration/lightspeed-stack-proper-name.yaml index 60add8957..5ede2acf5 100644 --- a/tests/configuration/lightspeed-stack-proper-name.yaml +++ b/tests/configuration/lightspeed-stack-proper-name.yaml @@ -22,11 +22,11 @@ service: - baz_header ogx: # Uses a remote OGX service - # The instance would have already been started with a llama-stack-run.yaml file + # The instance would have already been started with a run.yaml file use_as_library_client: false # Alternative for "as library use" # use_as_library_client: true - # library_client_config_path: + # library_client_config_path: url: http://localhost:8321 api_key: xyzzy user_data_collection: diff --git a/tests/configuration/lightspeed-stack.yaml b/tests/configuration/lightspeed-stack.yaml index b548c96fe..416647300 100644 --- a/tests/configuration/lightspeed-stack.yaml +++ b/tests/configuration/lightspeed-stack.yaml @@ -22,11 +22,11 @@ service: - baz_header ogx: # Uses a remote OGX service - # The instance would have already been started with a llama-stack-run.yaml file + # The instance would have already been started with a run.yaml file use_as_library_client: false # Alternative for "as library use" # use_as_library_client: true - # library_client_config_path: + # library_client_config_path: url: http://localhost:8321 api_key: xyzzy user_data_collection: diff --git a/tests/configuration/minimal-stack.yaml b/tests/configuration/minimal-stack.yaml index c0e5f5305..7ff9e4432 100644 --- a/tests/configuration/minimal-stack.yaml +++ b/tests/configuration/minimal-stack.yaml @@ -1,5 +1,5 @@ version: '2' -distro_name: llamastack-minimal-stack +distro_name: ogx-minimal-stack container_image: null external_providers_dir: /tmp @@ -9,10 +9,10 @@ storage: backends: kv_default: type: kv_sqlite - db_path: '/tmp/test_llama_stack_kv.db' + db_path: '/tmp/test_ogx_kv.db' sql_default: type: sql_sqlite - db_path: '/tmp/test_llama_stack_sql.db' + db_path: '/tmp/test_ogx_sql.db' stores: metadata: namespace: registry diff --git a/tests/configuration/run.yaml b/tests/configuration/run.yaml index 374dab491..1ccf733cc 100644 --- a/tests/configuration/run.yaml +++ b/tests/configuration/run.yaml @@ -1,5 +1,5 @@ version: '2' -distro_name: minimal-viable-llama-stack-configuration +distro_name: minimal-viable-ogx-configuration apis: - responses diff --git a/tests/e2e/configuration/README.md b/tests/e2e/configuration/README.md index c9dad6890..397d3088d 100644 --- a/tests/e2e/configuration/README.md +++ b/tests/e2e/configuration/README.md @@ -10,10 +10,11 @@ This directory contains configuration files used for end-to-end testing of Light ## Library mode uses unified configs (LCORE-2342) The library-mode configurations use the unified single-file format: instead of -the legacy `llama_stack.library_client_config_path`, they carry +the legacy `ogx.library_client_config_path` (the deprecated `llama_stack` YAML +section alias is still accepted), they carry ```yaml -llama_stack: +ogx: use_as_library_client: true config: profile: run.yaml diff --git a/tests/e2e/configuration/library-mode/lightspeed-stack-shields-empty.yaml b/tests/e2e/configuration/library-mode/lightspeed-stack-shields-empty.yaml index 17291966d..5a5021bf0 100644 --- a/tests/e2e/configuration/library-mode/lightspeed-stack-shields-empty.yaml +++ b/tests/e2e/configuration/library-mode/lightspeed-stack-shields-empty.yaml @@ -9,7 +9,7 @@ service: workers: 1 color_log: true access_log: true -llama_stack: +ogx: # Library mode - embeds OGX as library use_as_library_client: true # Unified mode: run.yaml (materialized per provider by CI/the harness) diff --git a/tests/e2e/configuration/library-mode/lightspeed-stack-shields-override-disabled.yaml b/tests/e2e/configuration/library-mode/lightspeed-stack-shields-override-disabled.yaml index 442b55ca2..f93d32297 100644 --- a/tests/e2e/configuration/library-mode/lightspeed-stack-shields-override-disabled.yaml +++ b/tests/e2e/configuration/library-mode/lightspeed-stack-shields-override-disabled.yaml @@ -11,7 +11,7 @@ service: workers: 1 color_log: true access_log: true -llama_stack: +ogx: # Library mode - embeds OGX as library use_as_library_client: true # Unified mode: run.yaml (materialized per provider by CI/the harness) diff --git a/tests/e2e/configuration/library-mode/lightspeed-stack-shields.yaml b/tests/e2e/configuration/library-mode/lightspeed-stack-shields.yaml index b00d2250e..b6c308d58 100644 --- a/tests/e2e/configuration/library-mode/lightspeed-stack-shields.yaml +++ b/tests/e2e/configuration/library-mode/lightspeed-stack-shields.yaml @@ -1,5 +1,5 @@ # @cfg_shields -# LCORE-owned safety shields (not Llama Stack / OGX Safety API resources). +# LCORE-owned safety shields (not OGX Safety API resources). # Configures one shield of each supported type so GET /v1/shields can be # asserted against both `question_validity` and `redaction` shapes. See # tests/e2e/features/shields.feature. @@ -11,7 +11,7 @@ service: workers: 1 color_log: true access_log: true -llama_stack: +ogx: # Library mode - embeds OGX as library use_as_library_client: true # Unified mode: run.yaml (materialized per provider by CI/the harness) diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-authorized.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-authorized.yaml index 946e606f6..941f5657b 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-authorized.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-authorized.yaml @@ -12,11 +12,11 @@ service: access_log: true ogx: # Uses a remote OGX service - # The instance would have already been started with a llama-stack-run.yaml file + # The instance would have already been started with a run.yaml file use_as_library_client: false # Alternative for "as library use" # use_as_library_client: true - # library_client_config_path: + # library_client_config_path: url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-negative.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-negative.yaml index 20a6c6f78..7d7832f11 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-negative.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-negative.yaml @@ -13,11 +13,11 @@ service: access_log: true ogx: # Uses a remote OGX service - # The instance would have already been started with a llama-stack-run.yaml file + # The instance would have already been started with a run.yaml file use_as_library_client: false # Alternative for "as library use" # use_as_library_client: true - # library_client_config_path: + # library_client_config_path: url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-shields-empty.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-shields-empty.yaml index 02eb06cea..1a5233af3 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-shields-empty.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-shields-empty.yaml @@ -9,7 +9,7 @@ service: workers: 1 color_log: true access_log: true -llama_stack: +ogx: # Server mode - connects to separate OGX service use_as_library_client: false url: http://${env.E2E_OGX_HOSTNAME}:8321 diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-shields-override-disabled.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-shields-override-disabled.yaml index 645117283..40220313b 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-shields-override-disabled.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-shields-override-disabled.yaml @@ -11,7 +11,7 @@ service: workers: 1 color_log: true access_log: true -llama_stack: +ogx: # Server mode - connects to separate OGX service use_as_library_client: false url: http://${env.E2E_OGX_HOSTNAME}:8321 diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-shields.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-shields.yaml index 2ea4827db..f0a9d33e4 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-shields.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-shields.yaml @@ -1,5 +1,5 @@ # @cfg_shields -# LCORE-owned safety shields (not Llama Stack / OGX Safety API resources). +# LCORE-owned safety shields (not OGX Safety API resources). # Configures one shield of each supported type so GET /v1/shields can be # asserted against both `question_validity` and `redaction` shapes. See # tests/e2e/features/shields.feature. @@ -11,7 +11,7 @@ service: workers: 1 color_log: true access_log: true -llama_stack: +ogx: # Server mode - connects to separate OGX service use_as_library_client: false url: http://${env.E2E_OGX_HOSTNAME}:8321 diff --git a/tests/e2e/features/environment.py b/tests/e2e/features/environment.py index 4fc2346fc..0fa71ac59 100644 --- a/tests/e2e/features/environment.py +++ b/tests/e2e/features/environment.py @@ -32,7 +32,6 @@ prepare_tls_feature_entry_on_prow, reset_tls_prow_state, ) -from tests.e2e.utils.ogx_utils import register_shield from tests.e2e.utils.prow_utils import ( restart_pod, restore_ogx_pod, @@ -252,15 +251,6 @@ def before_scenario(context: Context, scenario: Scenario) -> None: # Reset force-restart from a prior disrupt/MCP reset scenario. context.force_lightspeed_restart_after_mcp_config_reset = False - # Clear shield unregister state from previous scenarios (see ``shields_are_disabled_for_scenario``). - for _attr in ( - "shields_disabled_for_scenario", - "ogx_guard_provider_id", - "ogx_guard_provider_shield_id", - ): - if hasattr(context, _attr): - delattr(context, _attr) - def _dump_pod_logs_on_failure( context: Context, scenario: Scenario, namespace: str @@ -268,6 +258,7 @@ def _dump_pod_logs_on_failure( """Dump container logs when a scenario fails in Prow.""" if scenario.status != "failed": return + # Pod names match tests/e2e-prow manifests (legacy llama-stack-service id). pods: tuple[str, ...] = ("llama-stack-service", "lightspeed-stack-service") feature = getattr(context, "feature", None) feat_file = getattr(feature, "filename", "") or "" if feature else "" @@ -287,7 +278,7 @@ def _dump_pod_logs_on_failure( def after_scenario(context: Context, scenario: Scenario) -> None: """Run after each scenario is run. - Perform per-scenario teardown: failure logs (Prow) and shield re-register. + Perform per-scenario teardown: failure logs (Prow). If ``configure_service`` applied a non-baseline YAML during the scenario (``context.scenario_lightspeed_override_active``), clears that flag only; @@ -304,7 +295,7 @@ def after_scenario(context: Context, scenario: Scenario) -> None: running before the scenario. - hostname_ogx, port_ogx (str/int, optional): host and port used for the OGX health check. - scenario (Scenario): Behave scenario (unused; shield restore uses context flags). + scenario (Scenario): Behave scenario used for failure log dumps in Prow. """ if is_prow_environment(): _dump_pod_logs_on_failure( @@ -314,21 +305,6 @@ def after_scenario(context: Context, scenario: Scenario) -> None: if getattr(context, "scenario_lightspeed_override_active", False): context.scenario_lightspeed_override_active = False - # Re-register shield if ``Given shields are disabled for this scenario`` unregistered it. - if getattr(context, "shields_disabled_for_scenario", False): - provider_id = getattr(context, "ogx_guard_provider_id", None) - provider_shield_id = getattr(context, "ogx_guard_provider_shield_id", None) - if provider_id is not None and provider_shield_id is not None: - try: - register_shield( - "llama-guard", - provider_id=provider_id, - provider_shield_id=provider_shield_id, - ) - print("Re-registered shield llama-guard") - except (TypeError, ValueError, RuntimeError, KeyboardInterrupt) as e: - print(f"Warning: Could not re-register shield: {e}") - def _print_ogx_diagnostics() -> None: """Print container state, health, and recent logs to diagnose why OGX did not recover.""" diff --git a/tests/e2e/features/proxy.feature b/tests/e2e/features/proxy.feature index 813011e79..0eb5edc7b 100644 --- a/tests/e2e/features/proxy.feature +++ b/tests/e2e/features/proxy.feature @@ -5,9 +5,9 @@ Feature: Proxy and TLS networking tests for OGX providers remote inference providers are configured with proxy and TLS settings via the run.yaml NetworkConfig. - Query bodies use shield_ids: [] because Llama Guard moderation can issue - separate provider calls inside OGX that may not inherit the same - proxy/TLS CA trust as the scenario's remote inference provider. + Query bodies use shield_ids: [] so LCORE-owned shields (e.g. pii-redaction in + the default config) do not run; scenarios then exercise only the remote + inference provider's proxy/TLS path. Background: Given The service is started locally diff --git a/tests/e2e/features/query.feature b/tests/e2e/features/query.feature index 67fd86322..8bdb7b701 100644 --- a/tests/e2e/features/query.feature +++ b/tests/e2e/features/query.feature @@ -317,7 +317,6 @@ Scenario: Check if LLM responds for query request with error for missing query #https://issues.redhat.com/browse/LCORE-1387 @skip Scenario: Check if query without shields returns 413 when question is too long for model context - Given shields are disabled for this scenario When I use "query" to ask question with too-long query and authorization header Then The status code of the response is 413 And The body of the response contains Prompt is too long diff --git a/tests/e2e/features/steps/README.md b/tests/e2e/features/steps/README.md index db2233cfe..1c4125b5f 100644 --- a/tests/e2e/features/steps/README.md +++ b/tests/e2e/features/steps/README.md @@ -64,10 +64,6 @@ Behave steps for POST /v1/responses (LCORE Responses API) multi-turn tests. rlsapi v1 endpoint test steps. -## [shields.py](shields.py) - -Behave steps for temporarily disabling OGX shields in e2e (server mode). - ## [tls.py](tls.py) Step definitions for TLS configuration e2e tests. diff --git a/tests/e2e/features/steps/shields.py b/tests/e2e/features/steps/shields.py deleted file mode 100644 index 1ac4f4443..000000000 --- a/tests/e2e/features/steps/shields.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Behave steps for temporarily disabling OGX shields in e2e (server mode).""" - -from behave import given # pyright: ignore[reportAttributeAccessIssue] -from behave.runner import Context - -from tests.e2e.utils.ogx_utils import unregister_shield - - -@given("shields are disabled for this scenario") -def shields_are_disabled_for_scenario(context: Context) -> None: - """Unregister ``llama-guard`` for this scenario; ``after_scenario`` restores it when possible. - - Sets ``context.shields_disabled_for_scenario`` so ``environment.after_scenario`` - re-registers the shield. **Server mode only**; in library mode the scenario is skipped - (no separate OGX to call). - - Parameters: - ---------- - context: Behave context; must expose ``is_library_mode`` and ``scenario``. - """ - if context.is_library_mode: - context.scenario.skip( - "Shield unregister/register only applies in server mode (OGX as a " - "separate service). In library mode the app's shields cannot be disabled from e2e." - ) - return - - try: - saved = unregister_shield("llama-guard") - context.ogx_guard_provider_id = saved[0] if saved else None - context.ogx_guard_provider_shield_id = saved[1] if saved else None - context.shields_disabled_for_scenario = True - print("Unregistered shield llama-guard for this scenario") - except Exception as e: # pylint: disable=broad-exception-caught - context.scenario.skip(f"Could not unregister shield (is OGX reachable?): {e}") diff --git a/tests/e2e/features/streaming_query.feature b/tests/e2e/features/streaming_query.feature index 24f78d398..5455fbe79 100644 --- a/tests/e2e/features/streaming_query.feature +++ b/tests/e2e/features/streaming_query.feature @@ -282,8 +282,9 @@ Feature: streaming_query endpoint API tests Then The status code of the response is 413 And The body of the response contains Prompt is too long + # https://issues.redhat.com/browse/LCORE-1387 + @skip Scenario: Check if streaming_query without shields returns 200 and error in stream when question is too long for model context - Given shields are disabled for this scenario When I use "streaming_query" to ask question with too-long query and authorization header Then The status code of the response is 200 And The streamed response contains error message Prompt is too long diff --git a/tests/e2e/utils/README.md b/tests/e2e/utils/README.md index 1f7e3a4ac..6d8c8c55d 100644 --- a/tests/e2e/utils/README.md +++ b/tests/e2e/utils/README.md @@ -8,10 +8,6 @@ Helpers for reading and updating OGX run.yaml across environments. Thin Prow/OpenShift wrappers for OGX run.yaml ConfigMap operations. -## [ogx_utils.py](ogx_utils.py) - -E2E test utilities for OGX shields. - ## [prow_utils.py](prow_utils.py) Prow/OpenShift-specific utility functions for E2E tests. diff --git a/tests/e2e/utils/ogx_config_utils.py b/tests/e2e/utils/ogx_config_utils.py index c4599c9f2..6002f20b8 100644 --- a/tests/e2e/utils/ogx_config_utils.py +++ b/tests/e2e/utils/ogx_config_utils.py @@ -27,14 +27,14 @@ def clear_ogx_config_backup() -> None: def reset_ogx_run_config_to_pipeline_default() -> None: - """Reset llama-stack-config run.yaml to Konflux/Prow pipeline seed (run-ci.yaml).""" + """Reset OGX run.yaml ConfigMap to Konflux/Prow pipeline seed (run-ci.yaml).""" if not is_prow_environment(): return run_ci = Path(__file__).resolve().parents[1] / "configs" / "run-ci.yaml" if not run_ci.is_file(): print(f"WARN: pipeline run.yaml seed not found at {run_ci}", flush=True) return - print(f"Resetting llama-stack-config from {run_ci.name}...", flush=True) + print(f"Resetting OGX run config from {run_ci.name}...", flush=True) update_ogx_run_configmap(str(run_ci)) diff --git a/tests/e2e/utils/ogx_utils.py b/tests/e2e/utils/ogx_utils.py deleted file mode 100644 index 5fa7d3f15..000000000 --- a/tests/e2e/utils/ogx_utils.py +++ /dev/null @@ -1,115 +0,0 @@ -"""E2E test utilities for OGX shields. - -This module provides functions to manage shields on a running OGX -instance during end-to-end tests: unregister/re-register shields (e.g. from the -``Given shields are disabled for this scenario`` step). - -Only applies when running OGX as a separate service (server mode). -Requires E2E_OGX_STACK_URL or E2E_OGX_HOSTNAME and E2E_OGX_PORT. -""" - -import asyncio -import os -from typing import Optional - -from ogx_client import ( - ApiException, - AsyncOgxClient, -) - -from tests.e2e.utils.utils import is_prow_environment - - -def _get_ogx_client() -> AsyncOgxClient: - """Build an AsyncOgxClient from env (for e2e test use).""" - base_url = os.getenv("E2E_OGX_STACK_URL") - if not base_url: - if is_prow_environment(): - host = os.getenv("E2E_OGX_HOSTNAME", "localhost") - else: - host = "localhost" - port = os.getenv("E2E_OGX_PORT", "8321") - base_url = f"http://{host}:{port}" - api_key = os.getenv("E2E_OGX_STACK_API_KEY", "xyzzy") - timeout = int(os.getenv("E2E_OGX_STACK_TIMEOUT", "60")) - return AsyncOgxClient(base_url=base_url, api_key=api_key, timeout=timeout) - - -# ----------------------------------------------------------------------------- -# Shields -# ----------------------------------------------------------------------------- - - -async def _unregister_shield_async(identifier: str) -> Optional[tuple[str, str]]: - """Unregister a shield by identifier; return (provider_id, provider_shield_id) for restore.""" - client = _get_ogx_client() - try: - shields = await client.shields.list() - provider_id = None - provider_shield_id = None - found = False - for shield in shields: - if getattr(shield, "identifier", None) == identifier: - provider_id = getattr(shield, "provider_id", None) - provider_shield_id = getattr( - shield, "provider_resource_id", None - ) or getattr(shield, "provider_shield_id", None) - found = True - break - if not found: - # Shield not registered; nothing to delete, scenario can proceed - return None - try: - await client.shields.delete(identifier) - except ApiException as e: - if not e.status: - raise - # 400 "not found": shield already absent, scenario can proceed - if e.status == 400 and "not found" in str(e).lower(): - return None - raise - if provider_id is not None and provider_shield_id is not None: - return (provider_id, provider_shield_id) - return None - finally: - await client.close() - - -async def _register_shield_async( - shield_id: str, - provider_id: str, - provider_shield_id: str, -) -> None: - """Register a shield (restore after unregister).""" - client = _get_ogx_client() - try: - await client.shields.register( - shield_id=shield_id, - provider_id=provider_id, - provider_shield_id=provider_shield_id, - ) - finally: - await client.close() - - -def unregister_shield( - identifier: str = "llama-guard", -) -> Optional[tuple[str, str]]: - """Unregister the shield via client.shields.delete(); return (provider_id, provider_shield_id).""" - return asyncio.run(_unregister_shield_async(identifier)) - - -def register_shield( - shield_id: str = "llama-guard", - provider_id: Optional[str] = None, - provider_shield_id: Optional[str] = None, -) -> None: - """Re-register the shield via client.shields.register().""" - if not provider_id: - provider_id = os.getenv("E2E_OGX_GUARD_PROVIDER_ID", "llama-guard") - if not provider_shield_id: - provider_shield_id = os.getenv( - "E2E_OGX_GUARD_PROVIDER_SHIELD_ID", - "openai/gpt-4o-mini", - ) - asyncio.run(_register_shield_async(shield_id, provider_id, provider_shield_id)) diff --git a/tests/e2e/utils/prow_utils.py b/tests/e2e/utils/prow_utils.py index ea7b1bb57..0dc855a3a 100644 --- a/tests/e2e/utils/prow_utils.py +++ b/tests/e2e/utils/prow_utils.py @@ -106,7 +106,7 @@ def restart_pod(container_name: str) -> None: if container_name in _OGX_RESTART_NAMES: op = "restart-ogx" # Subprocess cap must exceed e2e-ops internal waits (pod + in-pod health + port-forward). - # Konflux TLS full recreate: ~6–12 min typical, 15+ min under load (user-reported 400s+). + # Konflux TLS full recreate: ~6-12 min typical, 15+ min under load (user-reported 400s+). if os.environ.get("E2E_COPY_MOCK_TLS_CERTS_TO_OGX") == "1": timeout = 1200 elif os.environ.get("E2E_KONFLUX_E2E") == "1": @@ -115,7 +115,7 @@ def restart_pod(container_name: str) -> None: timeout = 420 elif container_name in _LIGHTSPEED_RESTART_NAMES: op = "restart-lightspeed" - # Konflux LCS: TCP readiness + Llama handshake; full recreate can exceed 10 min under load. + # Konflux LCS: TCP readiness + OGX handshake; full recreate can exceed 10 min under load. timeout = 1200 if os.environ.get("E2E_KONFLUX_E2E") == "1" else 320 else: print( diff --git a/tests/integration/test_unified_synthesis.py b/tests/integration/test_unified_synthesis.py index fa7c8d1e6..33a08b279 100644 --- a/tests/integration/test_unified_synthesis.py +++ b/tests/integration/test_unified_synthesis.py @@ -35,7 +35,7 @@ # A complete, valid lightspeed-stack.yaml used as the base for configs that # are loaded through the real AppConfig.load_configuration pipeline; -# individual tests override its llama_stack / inference sections. +# individual tests override its ogx / inference sections. _BASE_CONFIG_PATH = "tests/configuration/lightspeed-stack.yaml" # A representative operator-authored legacy run.yaml. It deliberately carries @@ -471,7 +471,7 @@ def test_migrate_then_synthesize_preserves_enrichment_parity( def test_load_rejects_config_block_and_legacy_path_together( tmp_path: Path, ) -> None: - """A llama_stack.config block plus a legacy path fails the real load (R3).""" + """An ogx.config block plus a legacy path fails the real load (R3).""" lcs_dict = _base_config_dict() lcs_dict["ogx"] = { "use_as_library_client": True, diff --git a/tests/unit/telemetry/conftest.py b/tests/unit/telemetry/conftest.py index 903a9170c..a13f43647 100644 --- a/tests/unit/telemetry/conftest.py +++ b/tests/unit/telemetry/conftest.py @@ -241,8 +241,8 @@ ], "safety": [ { - "provider_id": "llama-guard", - "provider_type": "inline::llama-guard", + "provider_id": "content-filter", + "provider_type": "inline::content-filter", "config": {}, }, ], @@ -264,7 +264,7 @@ }, ], "shields": [ - {"shield_id": "llama-guard", "provider_id": "llama-guard"}, + {"shield_id": "content-filter", "provider_id": "content-filter"}, ], "vector_stores": [], }, diff --git a/tests/unit/test_ogx_synthesize.py b/tests/unit/test_ogx_synthesize.py index 88d339465..2cefb8468 100644 --- a/tests/unit/test_ogx_synthesize.py +++ b/tests/unit/test_ogx_synthesize.py @@ -223,9 +223,9 @@ def test_default_baseline_resolves_when_openai_api_key_set( ({"a": 1}, {"a": 2}, {"a": 2}), # maps merge recursively, untouched keys preserved ( - {"safety": {"default_shield_id": "llama-guard", "x": 1}}, + {"safety": {"default_shield_id": "content-filter", "x": 1}}, {"safety": {"x": 2}}, - {"safety": {"default_shield_id": "llama-guard", "x": 2}}, + {"safety": {"default_shield_id": "content-filter", "x": 2}}, ), # lists replace wholesale (no append) ( @@ -1002,7 +1002,7 @@ def test_synthesize_to_file_tightens_perms_on_overwrite(tmp_path: Path) -> None: }, ], }, - "safety": {"default_shield_id": "llama-guard", "excluded_categories": []}, + "safety": {"default_shield_id": "content-filter", "excluded_categories": []}, } From 1211a79486de9d1c1a8ef4ea2ba9156b40fa79fb Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Mon, 14 Sep 2026 09:02:05 +0200 Subject: [PATCH 090/120] LCORE-3580: Updated devel doc --- src/utils/README.md | 16 ---------------- src/utils/mcp/README.md | 18 ++++++++++++++++++ tests/unit/app/endpoints/README.md | 4 ++++ tests/unit/utils/README.md | 12 ------------ tests/unit/utils/mcp/README.md | 14 ++++++++++++++ 5 files changed, 36 insertions(+), 28 deletions(-) create mode 100644 src/utils/mcp/README.md create mode 100644 tests/unit/utils/mcp/README.md diff --git a/src/utils/README.md b/src/utils/README.md index 4763ed133..fb2dceb04 100644 --- a/src/utils/README.md +++ b/src/utils/README.md @@ -52,22 +52,6 @@ Function to transform a JSON Schema-like dictionary into an OpenAPI-compatible s Utilities for repairing truncated markdown content. -## [mcp_auth_headers.py](mcp_auth_headers.py) - -Utilities for resolving MCP server authorization headers. - -## [mcp_headers.py](mcp_headers.py) - -MCP headers handling. - -## [mcp_oauth_probe.py](mcp_oauth_probe.py) - -Probe MCP servers for OAuth and raise 401 with WWW-Authenticate when required. - -## [mcp_tools.py](mcp_tools.py) - -Utilities for discovering tools from remote MCP servers without OGX. - ## [model_list.py](model_list.py) Helpers for normalizing OGX ``models.list()`` union responses. diff --git a/src/utils/mcp/README.md b/src/utils/mcp/README.md new file mode 100644 index 000000000..75b4ab996 --- /dev/null +++ b/src/utils/mcp/README.md @@ -0,0 +1,18 @@ +# List of source files stored in `src/utils/mcp` directory + +## [mcp_auth_headers.py](mcp_auth_headers.py) + +Utilities for resolving MCP server authorization headers. + +## [mcp_headers.py](mcp_headers.py) + +MCP headers handling. + +## [mcp_oauth_probe.py](mcp_oauth_probe.py) + +Probe MCP servers for OAuth and raise 401 with WWW-Authenticate when required. + +## [mcp_tools.py](mcp_tools.py) + +Utilities for discovering tools from remote MCP servers without OGX. + diff --git a/tests/unit/app/endpoints/README.md b/tests/unit/app/endpoints/README.md index 5493e35c4..e9dd97e91 100644 --- a/tests/unit/app/endpoints/README.md +++ b/tests/unit/app/endpoints/README.md @@ -72,6 +72,10 @@ Unit tests for the /providers REST API endpoints. Unit tests for the /query (v2) REST API endpoint using Responses API. +## [test_query_otel.py](test_query_otel.py) + +OpenTelemetry unit tests for the /query REST API endpoint. + ## [test_rags.py](test_rags.py) Unit tests for the /rags REST API endpoints. diff --git a/tests/unit/utils/README.md b/tests/unit/utils/README.md index c5c81aaf3..f18d2cca0 100644 --- a/tests/unit/utils/README.md +++ b/tests/unit/utils/README.md @@ -48,18 +48,6 @@ Unit tests for utils/json_schema_updater module. Unit tests for markdown repair utilities. -## [test_mcp_auth_headers.py](test_mcp_auth_headers.py) - -Unit tests for MCP authorization headers utilities. - -## [test_mcp_headers.py](test_mcp_headers.py) - -Unit tests for MCP headers utility functions. - -## [test_mcp_tools.py](test_mcp_tools.py) - -Unit tests for MCP tool discovery utilities. - ## [test_model_list.py](test_model_list.py) Unit tests for utils/model_list.py helpers. diff --git a/tests/unit/utils/mcp/README.md b/tests/unit/utils/mcp/README.md new file mode 100644 index 000000000..118713cd8 --- /dev/null +++ b/tests/unit/utils/mcp/README.md @@ -0,0 +1,14 @@ +# List of source files stored in `tests/unit/utils/mcp` directory + +## [test_mcp_auth_headers.py](test_mcp_auth_headers.py) + +Unit tests for MCP authorization headers utilities. + +## [test_mcp_headers.py](test_mcp_headers.py) + +Unit tests for MCP headers utility functions. + +## [test_mcp_tools.py](test_mcp_tools.py) + +Unit tests for MCP tool discovery utilities. + From a0fa7f36dbc0ae483207aa439cb785c62fd5d76a Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Mon, 14 Sep 2026 09:05:40 +0200 Subject: [PATCH 091/120] LCORE-3580: Added devel deps used by benchmarks --- pyproject.toml | 2 ++ uv.lock | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 2c590cb96..02cf39296 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -152,6 +152,8 @@ dev = [ "pytest-benchmark>=5.2.3", "pyroscope-io>=0.8.7", "memray>=1.13.0", + "pygal>=3.1.3", + "pygaljs>=1.0.2", ] ogxlibdev = [ # To check OGX API provider dependecies: diff --git a/uv.lock b/uv.lock index 7575cd16c..7d6445c00 100644 --- a/uv.lock +++ b/uv.lock @@ -2250,6 +2250,8 @@ dev = [ { name = "pip" }, { name = "pybuild-deps" }, { name = "pydocstyle" }, + { name = "pygal" }, + { name = "pygaljs" }, { name = "pylint" }, { name = "pyright" }, { name = "pyroscope-io" }, @@ -2364,6 +2366,8 @@ dev = [ { name = "pip", specifier = "==26.1" }, { name = "pybuild-deps", specifier = ">=0.5.0" }, { name = "pydocstyle", specifier = ">=6.3.0" }, + { name = "pygal", specifier = ">=3.1.3" }, + { name = "pygaljs", specifier = ">=1.0.2" }, { name = "pylint", specifier = ">=3.3.7" }, { name = "pyright", specifier = ">=1.1.401" }, { name = "pyroscope-io", specifier = ">=0.8.7" }, @@ -4104,6 +4108,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/ea/99ddefac41971acad68f14114f38261c1f27dac0b3ec529824ebc739bdaa/pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019", size = 38038, upload-time = "2023-01-17T20:29:18.094Z" }, ] +[[package]] +name = "pygal" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/b9/58bbfa4d0e2672b0f0baf84047b17d58fda0d9351427e2b6784899924a1b/pygal-3.1.3.tar.gz", hash = "sha256:dd119c14cdeb56beb85282e3e2687ece30561225d410a822e6bb68699aa6e7b1", size = 80887, upload-time = "2026-06-18T20:44:51.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/43/5441ea0f9a35a0f2d30a79712cc21c737cf959a3451565d41e09d2fd90de/pygal-3.1.3-py3-none-any.whl", hash = "sha256:c0b9bc2d31df4094c9f65b0969b62571a47b28197aced081b1a9433c3a760f32", size = 132630, upload-time = "2026-06-18T20:44:49.643Z" }, +] + +[[package]] +name = "pygaljs" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/19/3a53f34232a9e6ddad665e71c83693c5db9a31f71785105905c5bc9fbbba/pygaljs-1.0.2.tar.gz", hash = "sha256:0b71ee32495dcba5fbb4a0476ddbba07658ad65f5675e4ad409baf154dec5111", size = 89711, upload-time = "2020-04-03T07:51:44.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/6f/07dab31ca496feda35cf3455b9e9380c43b5c685bb54ad890831c790da38/pygaljs-1.0.2-py2.py3-none-any.whl", hash = "sha256:d75e18cb21cc2cda40c45c3ee690771e5e3d4652bf57206f20137cf475c0dbe8", size = 91111, upload-time = "2020-04-03T07:51:42.658Z" }, +] + [[package]] name = "pygments" version = "2.21.0" From b173bbf5073649902d83df78d0cfe16adfb53130 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Mon, 14 Sep 2026 09:07:43 +0200 Subject: [PATCH 092/120] LCORE-3819: Fixed typo in config --- docs/devel_doc/openapi.json | 2 +- docs/user_doc/config.html | 2 +- docs/user_doc/config.json | 2 +- docs/user_doc/config.md | 16 ++++++++-------- src/models/config.py | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/devel_doc/openapi.json b/docs/devel_doc/openapi.json index eed3b377f..94b797bf2 100644 --- a/docs/devel_doc/openapi.json +++ b/docs/devel_doc/openapi.json @@ -14692,7 +14692,7 @@ }, "type": "array", "title": "Defined risks", - "description": "Risks to be considered while applying this guradrail" + "description": "Risks to be considered while applying this guardrail" } }, "additionalProperties": false, diff --git a/docs/user_doc/config.html b/docs/user_doc/config.html index 7a9f4eabd..258d105ed 100644 --- a/docs/user_doc/config.html +++ b/docs/user_doc/config.html @@ -1030,7 +1030,7 @@

    GraniteGuardianConfig

  • True: Verify using system CA bundle (default, recommended)
  • False: Disable verification (insecure, for dev only)
  • str: Path to custom CA bundle file (for internal PKI) | | risks | -array | Risks to be considered while applying this guradrail |
  • +array | Risks to be considered while applying this guardrail |

GraniteGuardianShieldConfiguration

Configuration for a named Granite Guardian guardrail shield.

diff --git a/docs/user_doc/config.json b/docs/user_doc/config.json index 1eef80d5e..268155e15 100644 --- a/docs/user_doc/config.json +++ b/docs/user_doc/config.json @@ -913,7 +913,7 @@ "title": "Verify SSL" }, "risks": { - "description": "Risks to be considered while applying this guradrail", + "description": "Risks to be considered while applying this guardrail", "items": { "$ref": "`#/components/schemas/`RiskDefinition" }, diff --git a/docs/user_doc/config.md b/docs/user_doc/config.md index eca3a16c0..bc9e32968 100644 --- a/docs/user_doc/config.md +++ b/docs/user_doc/config.md @@ -339,14 +339,14 @@ Storage config for a FAISS dynamic vector-store provider. Configuration for the Granite Guardian moderation guardrail. -| Field | Type | Description | -|-------------|---------|----------------------------------------------| -| url | string | The model_id to use for the guard | -| api_key | string | API key for the inference | -| max_retries | integer | Maximun number of retires | -| timeout | integer | Request timeout in seconds | -| verify_ssl | | SSL certificate verification | -| risks | array | Risks to be considered while applying this guradrail | +| Field | Type | Description | +|-------------|---------|------------------------------------------------------| +| url | string | The model_id to use for the guard | +| api_key | string | API key for the inference | +| max_retries | integer | Maximun number of retires | +| timeout | integer | Request timeout in seconds | +| verify_ssl | | SSL certificate verification | +| risks | array | Risks to be considered while applying this guardrail | ## GraniteGuardianShieldConfiguration diff --git a/src/models/config.py b/src/models/config.py index bcc2bf8ec..ce60fa5cd 100644 --- a/src/models/config.py +++ b/src/models/config.py @@ -3246,7 +3246,7 @@ class GraniteGuardianConfig(ConfigurationBase): risks: list[RiskDefinition] = Field( ..., title="Defined risks", - description="Risks to be considered while applying this guradrail", + description="Risks to be considered while applying this guardrail", ) From 4e3ef7e1ad54ad3c030c56b1c1186141cfedd971 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Mon, 14 Sep 2026 09:32:32 +0200 Subject: [PATCH 093/120] LCORE-3819: Use proper Responses type --- src/app/endpoints/a2a_openapi.py | 5 +++-- src/app/endpoints/authorized.py | 5 +++-- src/app/endpoints/config.py | 5 +++-- src/app/endpoints/conversations_v1.py | 9 +++++---- src/app/endpoints/conversations_v2.py | 9 +++++---- src/app/endpoints/feedback.py | 7 ++++--- src/app/endpoints/health.py | 7 ++++--- src/app/endpoints/info.py | 5 +++-- src/app/endpoints/mcp_auth.py | 5 +++-- src/app/endpoints/mcp_servers.py | 9 +++++---- src/app/endpoints/metrics.py | 5 +++-- src/app/endpoints/models.py | 5 +++-- src/app/endpoints/prompts.py | 13 +++++++------ src/app/endpoints/providers.py | 5 +++-- src/app/endpoints/query.py | 5 +++-- src/app/endpoints/rags.py | 7 ++++--- src/app/endpoints/responses.py | 3 ++- src/app/endpoints/rlsapi_v1.py | 3 ++- src/app/endpoints/root.py | 5 +++-- src/app/endpoints/saved_prompts.py | 11 ++++++----- src/app/endpoints/shields.py | 5 +++-- src/app/endpoints/skills.py | 5 +++-- src/app/endpoints/stream_interrupt.py | 5 +++-- src/app/endpoints/streaming_query.py | 5 +++-- src/app/endpoints/tools.py | 5 +++-- src/app/endpoints/vector_stores.py | 17 +++++++++-------- src/utils/types.py | 4 +++- 27 files changed, 101 insertions(+), 73 deletions(-) diff --git a/src/app/endpoints/a2a_openapi.py b/src/app/endpoints/a2a_openapi.py index e5990e665..4f4fc925e 100644 --- a/src/app/endpoints/a2a_openapi.py +++ b/src/app/endpoints/a2a_openapi.py @@ -1,11 +1,12 @@ """OpenAPI-only metadata for A2A JSON-RPC routes.""" -from typing import Any, Final +from typing import Final from constants import MEDIA_TYPE_EVENT_STREAM, MEDIA_TYPE_JSON +from utils.types import Responses # 200 may be buffered JSON-RPC (application/json) or SSE (text/event-stream). -a2a_jsonrpc_responses: Final[dict[int | str, dict[str, Any]]] = { +a2a_jsonrpc_responses: Final[Responses] = { 200: { "description": "Successful response", "content": { diff --git a/src/app/endpoints/authorized.py b/src/app/endpoints/authorized.py index 34b882b65..4930273bf 100644 --- a/src/app/endpoints/authorized.py +++ b/src/app/endpoints/authorized.py @@ -1,6 +1,6 @@ """Handler for REST API call to authorized endpoint.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends from opentelemetry import trace @@ -16,12 +16,13 @@ ) from models.api.responses.successful import AuthorizedResponse from utils.otel_tracing import SpanAttributes, anonymize_value, set_span_attributes +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["authorized"]) -authorized_responses: dict[int | str, dict[str, Any]] = { +authorized_responses: Responses = { 200: AuthorizedResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/config.py b/src/app/endpoints/config.py index 8102bb2b1..425790efb 100644 --- a/src/app/endpoints/config.py +++ b/src/app/endpoints/config.py @@ -1,6 +1,6 @@ """Handler for REST API call to retrieve service configuration.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends, Request from opentelemetry import trace @@ -20,13 +20,14 @@ from models.api.responses.successful import ConfigurationResponse from models.config import Action from utils.endpoints import check_configuration_loaded +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["config"]) -get_config_responses: dict[int | str, dict[str, Any]] = { +get_config_responses: Responses = { 200: ConfigurationResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/conversations_v1.py b/src/app/endpoints/conversations_v1.py index 4edfaac16..d744db5f4 100644 --- a/src/app/endpoints/conversations_v1.py +++ b/src/app/endpoints/conversations_v1.py @@ -52,12 +52,13 @@ normalize_conversation_id, to_ogx_conversation_id, ) +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["conversations_v1"]) -conversation_get_responses: dict[int | str, dict[str, Any]] = { +conversation_get_responses: Responses = { 200: ConversationResponse.openapi_response(), 400: BadRequestResponse.openapi_response(examples=["conversation_id"]), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), @@ -71,7 +72,7 @@ ), } -conversation_delete_responses: dict[int | str, dict[str, Any]] = { +conversation_delete_responses: Responses = { 200: ConversationDeleteResponse.openapi_response(), 400: BadRequestResponse.openapi_response(examples=["conversation_id"]), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), @@ -86,7 +87,7 @@ ), } -conversations_list_responses: dict[int | str, dict[str, Any]] = { +conversations_list_responses: Responses = { 200: ConversationsListResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -98,7 +99,7 @@ ), } -conversation_update_responses: dict[int | str, dict[str, Any]] = { +conversation_update_responses: Responses = { 200: ConversationUpdateResponse.openapi_response(), 400: BadRequestResponse.openapi_response(examples=["conversation_id"]), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), diff --git a/src/app/endpoints/conversations_v2.py b/src/app/endpoints/conversations_v2.py index 8905303dc..0478caa6d 100644 --- a/src/app/endpoints/conversations_v2.py +++ b/src/app/endpoints/conversations_v2.py @@ -33,13 +33,14 @@ from models.config import Action from utils.endpoints import check_configuration_loaded from utils.suid import check_suid +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["conversations_v2"]) -conversation_get_responses: dict[int | str, dict[str, Any]] = { +conversation_get_responses: Responses = { 200: ConversationResponse.openapi_response(), 400: BadRequestResponse.openapi_response(examples=["conversation_id"]), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), @@ -51,7 +52,7 @@ 503: ServiceUnavailableResponse.openapi_response(examples=["kubernetes api"]), } -conversation_delete_responses: dict[int | str, dict[str, Any]] = { +conversation_delete_responses: Responses = { 200: ConversationDeleteResponse.openapi_response(), 400: BadRequestResponse.openapi_response(examples=["conversation_id"]), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), @@ -62,7 +63,7 @@ 503: ServiceUnavailableResponse.openapi_response(examples=["kubernetes api"]), } -conversations_list_responses: dict[int | str, dict[str, Any]] = { +conversations_list_responses: Responses = { 200: ConversationsListResponseV2.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -72,7 +73,7 @@ 503: ServiceUnavailableResponse.openapi_response(examples=["kubernetes api"]), } -conversation_update_responses: dict[int | str, dict[str, Any]] = { +conversation_update_responses: Responses = { 200: ConversationUpdateResponse.openapi_response(), 400: BadRequestResponse.openapi_response(examples=["conversation_id"]), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), diff --git a/src/app/endpoints/feedback.py b/src/app/endpoints/feedback.py index d593aa917..77162e4ea 100644 --- a/src/app/endpoints/feedback.py +++ b/src/app/endpoints/feedback.py @@ -38,6 +38,7 @@ set_span_attributes, ) from utils.suid import get_suid +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) @@ -45,7 +46,7 @@ feedback_status_lock = threading.Lock() -feedback_post_response: dict[int | str, dict[str, Any]] = { +feedback_post_response: Responses = { 200: FeedbackResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint", "feedback"]), @@ -56,7 +57,7 @@ 503: ServiceUnavailableResponse.openapi_response(examples=["kubernetes api"]), } -feedback_put_response: dict[int | str, dict[str, Any]] = { +feedback_put_response: Responses = { 200: FeedbackStatusUpdateResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -64,7 +65,7 @@ 503: ServiceUnavailableResponse.openapi_response(examples=["kubernetes api"]), } -feedback_get_response: dict[int | str, dict[str, Any]] = { +feedback_get_response: Responses = { 200: StatusResponse.openapi_response(), } diff --git a/src/app/endpoints/health.py b/src/app/endpoints/health.py index cce30df60..7e0becbd0 100644 --- a/src/app/endpoints/health.py +++ b/src/app/endpoints/health.py @@ -5,7 +5,7 @@ methods. For HEAD HTTP method, just the HTTP response code is used. """ -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends, Response, status from ogx_client import ApiException @@ -33,13 +33,14 @@ ) from models.config import Action from utils.degraded_mode import DegradedModeTracker +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["health"]) -get_readiness_responses: dict[int | str, dict[str, Any]] = { +get_readiness_responses: Responses = { 200: ReadinessResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -48,7 +49,7 @@ ), } -get_liveness_responses: dict[int | str, dict[str, Any]] = { +get_liveness_responses: Responses = { 200: LivenessResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/info.py b/src/app/endpoints/info.py index 90b52baf4..5dd12177f 100644 --- a/src/app/endpoints/info.py +++ b/src/app/endpoints/info.py @@ -1,6 +1,6 @@ """Handler for REST API call to provide info.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Request from ogx_client import ApiException @@ -21,6 +21,7 @@ from models.api.responses.successful import InfoResponse from models.config import Action from utils.otel_tracing import set_span_attributes +from utils.types import Responses from version import __version__ logger = get_logger(__name__) @@ -28,7 +29,7 @@ router = APIRouter(tags=["info"]) -get_info_responses: dict[int | str, dict[str, Any]] = { +get_info_responses: Responses = { 200: InfoResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/mcp_auth.py b/src/app/endpoints/mcp_auth.py index 9cd7b326a..6327e7d6e 100644 --- a/src/app/endpoints/mcp_auth.py +++ b/src/app/endpoints/mcp_auth.py @@ -1,6 +1,6 @@ """Handler for REST API calls related to MCP server authentication.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends, Request from opentelemetry import trace @@ -23,13 +23,14 @@ from models.config import Action from utils.endpoints import check_configuration_loaded from utils.otel_tracing import SpanAttributes, set_span_attributes +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(prefix="/mcp-auth", tags=["mcp-auth"]) -mcp_auth_responses: dict[int | str, dict[str, Any]] = { +mcp_auth_responses: Responses = { 200: MCPClientAuthOptionsResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/mcp_servers.py b/src/app/endpoints/mcp_servers.py index 449336c85..6ac5bd8a6 100644 --- a/src/app/endpoints/mcp_servers.py +++ b/src/app/endpoints/mcp_servers.py @@ -1,6 +1,6 @@ """Handler for REST API calls to dynamically manage MCP servers.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Request, status from opentelemetry import trace @@ -27,13 +27,14 @@ from models.config import Action, ModelContextProtocolServer from utils.endpoints import check_configuration_loaded from utils.otel_tracing import SpanAttributes, set_span_attributes +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["mcp-servers"]) -register_responses: dict[int | str, dict[str, Any]] = { +register_responses: Responses = { 201: MCPServerRegistrationResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -107,7 +108,7 @@ async def register_mcp_server_handler( ) -list_responses: dict[int | str, dict[str, Any]] = { +list_responses: Responses = { 200: MCPServerListResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -161,7 +162,7 @@ async def list_mcp_servers_handler( return MCPServerListResponse(servers=servers) -delete_responses: dict[int | str, dict[str, Any]] = { +delete_responses: Responses = { 200: MCPServerDeleteResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint", "mcp server static"]), diff --git a/src/app/endpoints/metrics.py b/src/app/endpoints/metrics.py index a3a2e5742..22cd4aa12 100644 --- a/src/app/endpoints/metrics.py +++ b/src/app/endpoints/metrics.py @@ -1,6 +1,6 @@ """Handler for REST API call to provide metrics.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends, Request from fastapi.responses import PlainTextResponse @@ -21,12 +21,13 @@ UnauthorizedResponse, ) from models.config import Action +from utils.types import Responses tracer = trace.get_tracer(__name__) router = APIRouter(tags=["metrics"]) -metrics_get_responses: dict[int | str, dict[str, Any]] = { +metrics_get_responses: Responses = { 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), 500: InternalServerErrorResponse.openapi_response(examples=["configuration"]), diff --git a/src/app/endpoints/models.py b/src/app/endpoints/models.py index 57c2d4929..bd134594e 100644 --- a/src/app/endpoints/models.py +++ b/src/app/endpoints/models.py @@ -1,6 +1,6 @@ """Handler for REST API call to list available models.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, HTTPException, Query, Request from fastapi.params import Depends @@ -25,13 +25,14 @@ from models.config import Action from utils.endpoints import check_configuration_loaded from utils.model_list import parse_model_list_response +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["models"]) -models_responses: dict[int | str, dict[str, Any]] = { +models_responses: Responses = { 200: ModelsResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/prompts.py b/src/app/endpoints/prompts.py index 31f3c2fbc..9ecaab6f2 100644 --- a/src/app/endpoints/prompts.py +++ b/src/app/endpoints/prompts.py @@ -1,6 +1,6 @@ """Handler for REST API calls to manage OGX stored prompt templates.""" -from typing import Annotated, Any, Optional +from typing import Annotated, Optional from fastapi import APIRouter, Depends, HTTPException, Request from ogx_api import PromptNotFoundError, PromptVersionNotFoundError @@ -32,13 +32,14 @@ from utils.ogx_serialization import dump_ogx_model from utils.query import handle_known_apistatus_errors from utils.suid import check_suid_prompt +from utils.types import Responses logger = get_logger(__name__) router = APIRouter(tags=["prompts"]) # Response schemas for OpenAPI documentation -prompt_create_responses: dict[int | str, dict[str, Any]] = { +prompt_create_responses: Responses = { 200: PromptResourceResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint", "prompt manage"]), @@ -48,7 +49,7 @@ ), } -prompt_list_responses: dict[int | str, dict[str, Any]] = { +prompt_list_responses: Responses = { 200: PromptsListResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint", "prompt read"]), @@ -58,7 +59,7 @@ ), } -prompt_get_responses: dict[int | str, dict[str, Any]] = { +prompt_get_responses: Responses = { 200: PromptResourceResponse.openapi_response(), 400: BadRequestResponse.openapi_response(examples=["prompt_id"]), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), @@ -70,7 +71,7 @@ ), } -prompt_update_responses: dict[int | str, dict[str, Any]] = { +prompt_update_responses: Responses = { 200: PromptResourceResponse.openapi_response(), 400: BadRequestResponse.openapi_response(examples=["prompt_id"]), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), @@ -82,7 +83,7 @@ ), } -prompt_delete_responses: dict[int | str, dict[str, Any]] = { +prompt_delete_responses: Responses = { 200: PromptDeleteResponse.openapi_response(), 400: BadRequestResponse.openapi_response(examples=["prompt_id"]), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), diff --git a/src/app/endpoints/providers.py b/src/app/endpoints/providers.py index 0250ad41d..898c10cab 100644 --- a/src/app/endpoints/providers.py +++ b/src/app/endpoints/providers.py @@ -29,13 +29,14 @@ from models.config import Action from utils.endpoints import check_configuration_loaded from utils.ogx_serialization import dump_ogx_model +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["providers"]) -providers_list_responses: dict[int | str, dict[str, Any]] = { +providers_list_responses: Responses = { 200: ProvidersListResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -45,7 +46,7 @@ ), } -provider_get_responses: dict[int | str, dict[str, Any]] = { +provider_get_responses: Responses = { 200: ProviderResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/query.py b/src/app/endpoints/query.py index 8d5b9c47e..33865574a 100644 --- a/src/app/endpoints/query.py +++ b/src/app/endpoints/query.py @@ -1,7 +1,7 @@ """Handler for REST API call to provide answer to query using Response API.""" import datetime -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends, Request from opentelemetry import trace @@ -61,13 +61,14 @@ ) from utils.shields import run_shield_moderation, validate_shield_ids_override from utils.suid import normalize_conversation_id +from utils.types import Responses from utils.vector_search import build_rag_context logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["query"]) -query_response: dict[int | str, dict[str, Any]] = { +query_response: Responses = { 200: QueryResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response( examples=UNAUTHORIZED_OPENAPI_EXAMPLES_WITH_MCP_OAUTH diff --git a/src/app/endpoints/rags.py b/src/app/endpoints/rags.py index 6bd49d1d5..106a70701 100644 --- a/src/app/endpoints/rags.py +++ b/src/app/endpoints/rags.py @@ -1,6 +1,6 @@ """Handler for REST API calls to list and retrieve available RAGs.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, HTTPException, Request from fastapi.params import Depends @@ -27,13 +27,14 @@ ) from models.config import Action, RagStore from utils.endpoints import check_configuration_loaded +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["rags"]) -rags_responses: dict[int | str, dict[str, Any]] = { +rags_responses: Responses = { 200: RAGListResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -43,7 +44,7 @@ ), } -rag_responses: dict[int | str, dict[str, Any]] = { +rag_responses: Responses = { 200: RAGInfoResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/responses.py b/src/app/endpoints/responses.py index 9dc0f833b..33dadc561 100644 --- a/src/app/endpoints/responses.py +++ b/src/app/endpoints/responses.py @@ -116,6 +116,7 @@ normalize_conversation_id, ) from utils.tool_formatter import translate_vector_store_ids_to_user_facing +from utils.types import Responses from utils.vector_search import ( append_inline_rag_context_to_responses_input, build_rag_context, @@ -266,7 +267,7 @@ def _get_user_agent(request: Request) -> Optional[str]: return sanitized or None -responses_response: dict[int | str, dict[str, Any]] = { +responses_response: Responses = { 200: ResponsesResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response( examples=UNAUTHORIZED_OPENAPI_EXAMPLES_WITH_MCP_OAUTH diff --git a/src/app/endpoints/rlsapi_v1.py b/src/app/endpoints/rlsapi_v1.py index 81046f5a8..0baefbd69 100644 --- a/src/app/endpoints/rlsapi_v1.py +++ b/src/app/endpoints/rlsapi_v1.py @@ -73,6 +73,7 @@ from utils.rh_identity import AUTH_DISABLED, get_rh_identity_context from utils.shields import run_shield_moderation_v2 from utils.suid import get_suid +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) @@ -94,7 +95,7 @@ class TemplateRenderError(Exception): ) -infer_responses: dict[int | str, dict[str, Any]] = { +infer_responses: Responses = { 200: RlsapiV1InferResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/root.py b/src/app/endpoints/root.py index 87ae816ed..8dcc24a94 100644 --- a/src/app/endpoints/root.py +++ b/src/app/endpoints/root.py @@ -1,6 +1,6 @@ """Handler for the / endpoint.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends, Request from fastapi.responses import HTMLResponse @@ -17,6 +17,7 @@ UnauthorizedResponse, ) from models.config import Action +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) @@ -783,7 +784,7 @@ """ -root_responses: dict[int | str, dict[str, Any]] = { +root_responses: Responses = { 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), 503: ServiceUnavailableResponse.openapi_response(examples=["kubernetes api"]), diff --git a/src/app/endpoints/saved_prompts.py b/src/app/endpoints/saved_prompts.py index 750f6a6b6..f571a58ce 100644 --- a/src/app/endpoints/saved_prompts.py +++ b/src/app/endpoints/saved_prompts.py @@ -1,6 +1,6 @@ """Handler for REST API calls to manage saved prompts.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.concurrency import run_in_threadpool @@ -43,12 +43,13 @@ validate_saved_prompt_name, ) from utils.suid import check_suid +from utils.types import Responses logger = get_logger(__name__) router = APIRouter(tags=["saved-prompts"]) -get_saved_prompts_config_responses: dict[int | str, dict[str, Any]] = { +get_saved_prompts_config_responses: Responses = { 200: SavedPromptsConfigResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -56,7 +57,7 @@ 503: ServiceUnavailableResponse.openapi_response(examples=["kubernetes api"]), } -list_saved_prompts_responses: dict[int | str, dict[str, Any]] = { +list_saved_prompts_responses: Responses = { 200: SavedPromptsListResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -65,7 +66,7 @@ ), } -create_saved_prompts_responses: dict[int | str, dict[str, Any]] = { +create_saved_prompts_responses: Responses = { 201: SavedPromptResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -78,7 +79,7 @@ ), } -delete_saved_prompts_responses: dict[int | str, dict[str, Any]] = { +delete_saved_prompts_responses: Responses = { 200: SavedPromptDeleteResponse.openapi_response(), 400: BadRequestResponse.openapi_response(examples=["saved_prompt_id"]), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), diff --git a/src/app/endpoints/shields.py b/src/app/endpoints/shields.py index d432a8760..996fbbecb 100644 --- a/src/app/endpoints/shields.py +++ b/src/app/endpoints/shields.py @@ -1,6 +1,6 @@ """Handler for REST API call to list available shields.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Request from fastapi.params import Depends @@ -21,13 +21,14 @@ from models.common.shields import CatalogShield from models.config import Action from utils.endpoints import check_configuration_loaded +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["shields"]) -shields_responses: dict[int | str, dict[str, Any]] = { +shields_responses: Responses = { 200: ShieldsResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/skills.py b/src/app/endpoints/skills.py index 8fd8abdb8..e95b21e77 100644 --- a/src/app/endpoints/skills.py +++ b/src/app/endpoints/skills.py @@ -1,6 +1,6 @@ """Handler for REST API call to list loaded agent skills.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Request from fastapi.concurrency import run_in_threadpool @@ -21,12 +21,13 @@ from models.config import Action from utils.endpoints import check_configuration_loaded from utils.pydantic_ai_helpers import get_skills_metadata +from utils.types import Responses logger = get_logger(__name__) router = APIRouter(tags=["skills"]) -skills_responses: dict[int | str, dict[str, Any]] = { +skills_responses: Responses = { 200: SkillsResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/stream_interrupt.py b/src/app/endpoints/stream_interrupt.py index bc53358d9..0c3b8f6cf 100644 --- a/src/app/endpoints/stream_interrupt.py +++ b/src/app/endpoints/stream_interrupt.py @@ -1,6 +1,6 @@ """Endpoint for interrupting in-progress streaming query requests.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends, HTTPException from opentelemetry import trace @@ -25,11 +25,12 @@ StreamInterruptRegistry, get_stream_interrupt_registry, ) +from utils.types import Responses router = APIRouter(tags=["streaming_query_interrupt"]) tracer = trace.get_tracer(__name__) -stream_interrupt_responses: dict[int | str, dict[str, Any]] = { +stream_interrupt_responses: Responses = { 200: StreamingInterruptResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/streaming_query.py b/src/app/endpoints/streaming_query.py index a681f88fb..483639e25 100644 --- a/src/app/endpoints/streaming_query.py +++ b/src/app/endpoints/streaming_query.py @@ -3,7 +3,7 @@ import asyncio import datetime from collections.abc import AsyncIterator -from typing import Annotated, Any, Optional +from typing import Annotated, Optional from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import StreamingResponse @@ -94,6 +94,7 @@ stream_start_event, ) from utils.suid import get_suid, normalize_conversation_id +from utils.types import Responses from utils.vector_search import build_rag_context logger = get_logger(__name__) @@ -103,7 +104,7 @@ # Tracks background topic summary tasks for graceful shutdown. _background_topic_summary_tasks: list[asyncio.Task[None]] = [] -streaming_query_responses: dict[int | str, dict[str, Any]] = { +streaming_query_responses: Responses = { 200: StreamingQueryResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response( examples=UNAUTHORIZED_OPENAPI_EXAMPLES_WITH_MCP_OAUTH diff --git a/src/app/endpoints/tools.py b/src/app/endpoints/tools.py index 6a22ee471..4f2b87598 100644 --- a/src/app/endpoints/tools.py +++ b/src/app/endpoints/tools.py @@ -1,6 +1,6 @@ """Handler for REST API call to list available tools from MCP servers.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends, Request from opentelemetry import trace @@ -33,13 +33,14 @@ from utils.mcp.mcp_tools import list_mcp_tools from utils.pydantic_ai_helpers import get_agent_capability_tools from utils.tool_formatter import build_catalog_tool +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["tools"]) -tools_responses: dict[int | str, dict[str, Any]] = { +tools_responses: Responses = { 200: ToolsResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/vector_stores.py b/src/app/endpoints/vector_stores.py index 93fbf09ef..c5e75eccb 100644 --- a/src/app/endpoints/vector_stores.py +++ b/src/app/endpoints/vector_stores.py @@ -3,7 +3,7 @@ import asyncio import os from functools import lru_cache -from typing import Annotated, Any, Optional +from typing import Annotated, Optional from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile, status from ogx_client import ApiException, BadRequestError @@ -43,6 +43,7 @@ from models.config import Action from utils.endpoints import check_configuration_loaded from utils.query import handle_known_apistatus_errors +from utils.types import Responses logger = get_logger(__name__) router = APIRouter(tags=["vector-stores"]) @@ -70,7 +71,7 @@ def _get_vector_store_attach_semaphore() -> asyncio.Semaphore: # Response schemas for OpenAPI documentation -vector_stores_list_responses: dict[int | str, dict[str, Any]] = { +vector_stores_list_responses: Responses = { 200: VectorStoresListResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -80,7 +81,7 @@ def _get_vector_store_attach_semaphore() -> asyncio.Semaphore: ), } -vector_store_responses: dict[int | str, dict[str, Any]] = { +vector_store_responses: Responses = { 200: VectorStoreResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -91,7 +92,7 @@ def _get_vector_store_attach_semaphore() -> asyncio.Semaphore: ), } -file_responses: dict[int | str, dict[str, Any]] = { +file_responses: Responses = { 200: FileResponse.openapi_response(), 413: FileTooLargeResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), @@ -103,7 +104,7 @@ def _get_vector_store_attach_semaphore() -> asyncio.Semaphore: ), } -vector_store_file_responses: dict[int | str, dict[str, Any]] = { +vector_store_file_responses: Responses = { 200: VectorStoreFileResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -117,7 +118,7 @@ def _get_vector_store_attach_semaphore() -> asyncio.Semaphore: ), } -vector_store_files_list_responses: dict[int | str, dict[str, Any]] = { +vector_store_files_list_responses: Responses = { 200: VectorStoreFilesListResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -128,7 +129,7 @@ def _get_vector_store_attach_semaphore() -> asyncio.Semaphore: ), } -vector_store_delete_responses: dict[int | str, dict[str, Any]] = { +vector_store_delete_responses: Responses = { 200: VectorStoreDeleteResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -138,7 +139,7 @@ def _get_vector_store_attach_semaphore() -> asyncio.Semaphore: ), } -vector_store_file_delete_responses: dict[int | str, dict[str, Any]] = { +vector_store_file_delete_responses: Responses = { 200: VectorStoreFileDeleteResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/utils/types.py b/src/utils/types.py index 3847d1b71..6cf53582a 100644 --- a/src/utils/types.py +++ b/src/utils/types.py @@ -1,10 +1,12 @@ """Common types for the project.""" from re import Pattern -from typing import TypeVar, cast +from typing import Any, TypeVar, cast type SingletonInstances = dict[type, object] +type Responses = dict[int | str, dict[str, Any]] + CompiledPatterns = list[tuple[Pattern[str], str]] T = TypeVar("T") From 5a25ae2f002450a37e1c2bec084e4d1e1f85f764 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Mon, 14 Sep 2026 09:09:48 +0200 Subject: [PATCH 094/120] LCORE-4146: Typo in GraniteGuardianShieldConfiguration --- docs/devel_doc/openapi.json | 2 +- docs/models/successful_responses.json | 4 ++-- docs/models/successful_responses.md | 2 +- docs/user_doc/config.html | 2 +- docs/user_doc/config.json | 2 +- docs/user_doc/config.md | 16 ++++++++-------- src/models/config.py | 2 +- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/devel_doc/openapi.json b/docs/devel_doc/openapi.json index eed3b377f..b6e2b017c 100644 --- a/docs/devel_doc/openapi.json +++ b/docs/devel_doc/openapi.json @@ -14661,7 +14661,7 @@ "minimum": 0.0, "exclusiveMinimum": 0.0, "title": "Max retries", - "description": "Maximun number of retires", + "description": "Maximun number of retries", "default": 2 }, "timeout": { diff --git a/docs/models/successful_responses.json b/docs/models/successful_responses.json index ce326e5af..e6be288bb 100644 --- a/docs/models/successful_responses.json +++ b/docs/models/successful_responses.json @@ -1791,7 +1791,7 @@ }, "max_retries": { "default": 2, - "description": "Maximun number of retires", + "description": "Maximun number of retries", "minimum": 0, "maximum": 5, "title": "Max retries", @@ -7364,4 +7364,4 @@ } }, "paths": {} -} \ No newline at end of file +} diff --git a/docs/models/successful_responses.md b/docs/models/successful_responses.md index 099162736..06c8a4fd5 100644 --- a/docs/models/successful_responses.md +++ b/docs/models/successful_responses.md @@ -706,7 +706,7 @@ Configuration for the Granite Guardian moderation guardrail. |-------|------|-------------| | url | string | The model_id to use for the guard | | api_key | string | API key for the inference | -| max_retries | integer | Maximun number of retires | +| max_retries | integer | Maximun number of retries | | timeout | integer | Request timeout in seconds | | verify_ssl | | SSL certificate verification. Can be: - True: Verify using system CA bundle (default, recommended) diff --git a/docs/user_doc/config.html b/docs/user_doc/config.html index 7a9f4eabd..c1885cc14 100644 --- a/docs/user_doc/config.html +++ b/docs/user_doc/config.html @@ -1012,7 +1012,7 @@

GraniteGuardianConfig

max_retries integer - Maximun number of retires + Maximun number of retries timeout diff --git a/docs/user_doc/config.json b/docs/user_doc/config.json index 1eef80d5e..99352db40 100644 --- a/docs/user_doc/config.json +++ b/docs/user_doc/config.json @@ -885,7 +885,7 @@ }, "max_retries": { "default": 2, - "description": "Maximun number of retires", + "description": "Maximun number of retries", "minimum": 0, "maximum": 5, "title": "Max retries", diff --git a/docs/user_doc/config.md b/docs/user_doc/config.md index eca3a16c0..5af8d1bce 100644 --- a/docs/user_doc/config.md +++ b/docs/user_doc/config.md @@ -339,14 +339,14 @@ Storage config for a FAISS dynamic vector-store provider. Configuration for the Granite Guardian moderation guardrail. -| Field | Type | Description | -|-------------|---------|----------------------------------------------| -| url | string | The model_id to use for the guard | -| api_key | string | API key for the inference | -| max_retries | integer | Maximun number of retires | -| timeout | integer | Request timeout in seconds | -| verify_ssl | | SSL certificate verification | -| risks | array | Risks to be considered while applying this guradrail | +| Field | Type | Description | +|-------------|---------|------------------------------------------------------| +| url | string | The model_id to use for the guard | +| api_key | string | API key for the inference | +| max_retries | integer | Maximun number of retries | +| timeout | integer | Request timeout in seconds | +| verify_ssl | | SSL certificate verification | +| risks | array | Risks to be considered while applying this guradrail | ## GraniteGuardianShieldConfiguration diff --git a/src/models/config.py b/src/models/config.py index bcc2bf8ec..a9fc26de2 100644 --- a/src/models/config.py +++ b/src/models/config.py @@ -3225,7 +3225,7 @@ class GraniteGuardianConfig(ConfigurationBase): ) max_retries: PositiveInt = Field( - 2, ge=0, le=5, title="Max retries", description="Maximun number of retires" + 2, ge=0, le=5, title="Max retries", description="Maximun number of retries" ) timeout: PositiveInt = Field( From 541f8b930223fa36fda680c9064bb18d203423bb Mon Sep 17 00:00:00 2001 From: Andrej Simurka Date: Mon, 14 Sep 2026 10:57:10 +0200 Subject: [PATCH 095/120] Fixed MCP arguments processing --- src/utils/agents/tool_processor.py | 11 +++++++++-- .../unit/utils/agents/test_tool_processor.py | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/utils/agents/tool_processor.py b/src/utils/agents/tool_processor.py index 161cb05a6..2607ef1fe 100644 --- a/src/utils/agents/tool_processor.py +++ b/src/utils/agents/tool_processor.py @@ -29,7 +29,11 @@ ToolInfoSummary, ToolResultSummary, ) -from utils.responses import _build_okp_doc_url, resolve_source_for_result +from utils.responses import ( + _build_okp_doc_url, + parse_arguments_string, + resolve_source_for_result, +) logger = get_logger(__name__) @@ -95,10 +99,13 @@ def summarize_native_tool_call( ) # MCP call + tool_args = args.get("tool_args") or {} + if isinstance(tool_args, str): + tool_args = parse_arguments_string(tool_args) return ToolCallSummary( id=call_id, name=args.get("tool_name") or "", - args=args.get("tool_args", {}), + args=tool_args, type="mcp_call", ) case _: diff --git a/tests/unit/utils/agents/test_tool_processor.py b/tests/unit/utils/agents/test_tool_processor.py index bd77cf93d..e077c61f4 100644 --- a/tests/unit/utils/agents/test_tool_processor.py +++ b/tests/unit/utils/agents/test_tool_processor.py @@ -150,6 +150,25 @@ def test_mcp_call(self) -> None: assert summary.args == {"arg": 1} assert summary.type == "mcp_call" + def test_mcp_call_with_json_string_tool_args(self) -> None: + """Test MCP tool call parses OpenAI-style JSON string tool_args.""" + part = NativeToolCallPart( + tool_name=f"{MCPServerTool.kind}:srv", + args={ + "action": "call", + "tool_name": "get_subscriptions", + "tool_args": '{\n "limit": 20\n}', + }, + tool_call_id="mcp-call-json", + ) + + summary = summarize_native_tool_call(part) + + assert summary is not None + assert summary.name == "get_subscriptions" + assert summary.args == {"limit": 20} + assert summary.type == "mcp_call" + def test_unknown_tool_returns_none(self, mocker: MockerFixture) -> None: """Test unknown native tool logs warning and returns None.""" mock_warning = mocker.patch("utils.agents.tool_processor.logger.warning") From b893a2f1ee7a5072e9103656715174706892f623 Mon Sep 17 00:00:00 2001 From: Andrej Simurka Date: Mon, 14 Sep 2026 10:16:41 +0200 Subject: [PATCH 096/120] Deprecating vector_stores endpoints --- docs/devel_doc/openapi.json | 18 ++++++++++++++---- docs/migrations/v0.7.0.md | 12 ++++++++++++ src/app/endpoints/vector_stores.py | 27 +++++++++++++++++++++++++-- src/app/main.py | 8 +++++++- 4 files changed, 58 insertions(+), 7 deletions(-) diff --git a/docs/devel_doc/openapi.json b/docs/devel_doc/openapi.json index 94b797bf2..11aa59ff6 100644 --- a/docs/devel_doc/openapi.json +++ b/docs/devel_doc/openapi.json @@ -3844,7 +3844,8 @@ } } } - } + }, + "deprecated": true }, "post": { "tags": [ @@ -4063,7 +4064,8 @@ } } } - } + }, + "deprecated": true } }, "/v1/vector-stores/{vector_store_id}": { @@ -4074,6 +4076,7 @@ "summary": "Get Vector Store", "description": "Retrieve a vector store by ID.\n\nParameters:\n request: The incoming HTTP request.\n vector_store_id: ID of the vector store to retrieve.\n auth: Authentication tuple from the auth dependency.\n\nReturns:\n VectorStoreResponse: The vector store object.\n\nRaises:\n HTTPException:\n - 401: Authentication failed\n - 403: Authorization failed\n - 404: Vector store not found\n - 500: Lightspeed Stack configuration not loaded\n - 503: Unable to connect to OGX", "operationId": "get_vector_store_v1_vector_stores__vector_store_id__get", + "deprecated": true, "parameters": [ { "name": "vector_store_id", @@ -4294,6 +4297,7 @@ "summary": "Update Vector Store", "description": "Update a vector store.\n\nParameters:\n request: The incoming HTTP request.\n vector_store_id: ID of the vector store to update.\n auth: Authentication tuple from the auth dependency.\n body: Vector store update parameters.\n\nReturns:\n VectorStoreResponse: The updated vector store object.\n\nRaises:\n HTTPException:\n - 401: Authentication failed\n - 403: Authorization failed\n - 404: Vector store not found\n - 500: Lightspeed Stack configuration not loaded\n - 503: Unable to connect to OGX", "operationId": "update_vector_store_v1_vector_stores__vector_store_id__put", + "deprecated": true, "parameters": [ { "name": "vector_store_id", @@ -4524,6 +4528,7 @@ "summary": "Delete Vector Store", "description": "Delete a vector store.\n\nParameters:\n request: The incoming HTTP request.\n vector_store_id: ID of the vector store to delete.\n auth: Authentication tuple from the auth dependency.\n\nRaises:\n HTTPException:\n - 401: Authentication failed\n - 403: Authorization failed\n - 500: Lightspeed Stack configuration not loaded\n - 503: Unable to connect to OGX\n\nReturns:\n VectorStoreDeleteResponse: Delete outcome for the requested vector store.", "operationId": "delete_vector_store_v1_vector_stores__vector_store_id__delete", + "deprecated": true, "parameters": [ { "name": "vector_store_id", @@ -4958,7 +4963,8 @@ } } } - } + }, + "deprecated": true } }, "/v1/vector-stores/{vector_store_id}/files": { @@ -4969,6 +4975,7 @@ "summary": "Add File To Vector Store", "description": "Add a file to a vector store.\n\nParameters:\n request: The incoming HTTP request.\n vector_store_id: ID of the vector store.\n auth: Authentication tuple from the auth dependency.\n body: File addition parameters.\n\nReturns:\n VectorStoreFileResponse: The vector store file object.\n\nRaises:\n HTTPException:\n - 401: Authentication failed\n - 403: Authorization failed\n - 404: Vector store or file not found\n - 429: Too many concurrent vector store file attachments\n - 500: Lightspeed Stack configuration not loaded\n - 503: Unable to connect to OGX", "operationId": "add_file_to_vector_store_v1_vector_stores__vector_store_id__files_post", + "deprecated": true, "parameters": [ { "name": "vector_store_id", @@ -5214,6 +5221,7 @@ "summary": "List Vector Store Files", "description": "List files in a vector store.\n\nParameters:\n request: The incoming HTTP request.\n vector_store_id: ID of the vector store.\n auth: Authentication tuple from the auth dependency.\n\nReturns:\n VectorStoreFilesListResponse: List of files in the vector store.\n\nRaises:\n HTTPException:\n - 401: Authentication failed\n - 403: Authorization failed\n - 404: Vector store not found\n - 500: Lightspeed Stack configuration not loaded\n - 503: Unable to connect to OGX", "operationId": "list_vector_store_files_v1_vector_stores__vector_store_id__files_get", + "deprecated": true, "parameters": [ { "name": "vector_store_id", @@ -5441,6 +5449,7 @@ "summary": "Get Vector Store File", "description": "Retrieve a file from a vector store.\n\nParameters:\n request: The incoming HTTP request.\n vector_store_id: ID of the vector store.\n file_id: ID of the file.\n auth: Authentication tuple from the auth dependency.\n\nReturns:\n VectorStoreFileResponse: The vector store file object.\n\nRaises:\n HTTPException:\n - 401: Authentication failed\n - 403: Authorization failed\n - 404: File not found in vector store\n - 500: Lightspeed Stack configuration not loaded\n - 503: Unable to connect to OGX", "operationId": "get_vector_store_file_v1_vector_stores__vector_store_id__files__file_id__get", + "deprecated": true, "parameters": [ { "name": "vector_store_id", @@ -5685,6 +5694,7 @@ "summary": "Delete Vector Store File", "description": "Delete a file from a vector store.\n\nParameters:\n request: The incoming HTTP request.\n vector_store_id: ID of the vector store.\n file_id: ID of the file to delete.\n auth: Authentication tuple from the auth dependency.\n\nRaises:\n HTTPException:\n - 401: Authentication failed\n - 403: Authorization failed\n - 500: Lightspeed Stack configuration not loaded\n - 503: Unable to connect to OGX\n\nReturns:\n VectorStoreFileDeleteResponse: Delete outcome for the requested file.", "operationId": "delete_vector_store_file_v1_vector_stores__vector_store_id__files__file_id__delete", + "deprecated": true, "parameters": [ { "name": "vector_store_id", @@ -23771,7 +23781,7 @@ }, { "name": "vector-stores", - "description": "Vector stores and files." + "description": "Vector stores and files (OGX proxy). Deprecated: will be removed in the next LCS release." } ] } \ No newline at end of file diff --git a/docs/migrations/v0.7.0.md b/docs/migrations/v0.7.0.md index 202c4ca85..d2a26e0d2 100644 --- a/docs/migrations/v0.7.0.md +++ b/docs/migrations/v0.7.0.md @@ -4,6 +4,7 @@ * [RAG Configuration](#rag-configuration) * [OGX naming (`llama_stack` → `ogx`)](#ogx-naming-llama_stack--ogx) +* [Vector stores API deprecation](#vector-stores-api-deprecation) --- @@ -182,3 +183,14 @@ Chunk limits, currently hardcoded as constants, will be configurable fields in ` | `OKP_RAG_MAX_CHUNKS` | `rag.okp.max_chunks` | 5 | | `INLINE_RAG_MAX_CHUNKS` | `rag.retrieval.inline.max_chunks` | 10 | | `TOOL_RAG_MAX_CHUNKS` | `rag.retrieval.tool.max_chunks` | 10 | + +--- + +## Vector stores API deprecation + +The `/v1/vector-stores` and `/v1/files` routes proxy OGX vector-store and file +APIs. They are **deprecated in v0.7.0** and will be **removed in v0.8.0** when +OGX is dropped from Lightspeed Core Stack. + +Migrate to [BYOK RAG](../user_doc/byok_guide.md) for document retrieval. The +OpenAPI schema marks these operations as `deprecated`. diff --git a/src/app/endpoints/vector_stores.py b/src/app/endpoints/vector_stores.py index c5e75eccb..8ce1eff70 100644 --- a/src/app/endpoints/vector_stores.py +++ b/src/app/endpoints/vector_stores.py @@ -1,4 +1,13 @@ -"""Handler for REST API calls to manage vector stores and files.""" +"""Handler for REST API calls to manage vector stores and files. + +These routes proxy OGX vector-store and file APIs. They are deprecated and will +be removed in the next LCS release when OGX is dropped from the stack. Use BYOK +RAG configuration instead (see docs/user_doc/byok_guide.md). +""" + +VECTOR_STORES_DEPRECATED_REASON: str = ( + "OGX proxy API; deprecated and scheduled for removal in the next LCS release." +) import asyncio import os @@ -8,6 +17,7 @@ from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile, status from ogx_client import ApiException, BadRequestError from openai._exceptions import APIStatusError as OpenAIAPIStatusError +from typing_extensions import deprecated from authentication import get_auth_dependency from authentication.interface import AuthTuple @@ -46,7 +56,10 @@ from utils.types import Responses logger = get_logger(__name__) -router = APIRouter(tags=["vector-stores"]) +router = APIRouter( + tags=["vector-stores"], + deprecated=True, +) # Each upload/attach holds up to DEFAULT_MAX_FILE_UPLOAD_SIZE bytes in memory, # so unbounded concurrency multiplies memory usage linearly - these semaphores @@ -152,6 +165,7 @@ def _get_vector_store_attach_semaphore() -> asyncio.Semaphore: @router.post("/vector-stores", responses=vector_store_responses) @authorize(Action.MANAGE_VECTOR_STORES) +@deprecated(VECTOR_STORES_DEPRECATED_REASON) async def create_vector_store( request: Request, auth: Annotated[AuthTuple, Depends(get_auth_dependency())], @@ -230,6 +244,7 @@ async def create_vector_store( @router.get("/vector-stores", responses=vector_stores_list_responses) @authorize(Action.READ_VECTOR_STORES) +@deprecated(VECTOR_STORES_DEPRECATED_REASON) async def list_vector_stores( request: Request, auth: Annotated[AuthTuple, Depends(get_auth_dependency())], @@ -291,6 +306,7 @@ async def list_vector_stores( @router.get("/vector-stores/{vector_store_id}", responses=vector_store_responses) @authorize(Action.READ_VECTOR_STORES) +@deprecated(VECTOR_STORES_DEPRECATED_REASON) async def get_vector_store( request: Request, vector_store_id: str, @@ -356,6 +372,7 @@ async def get_vector_store( @router.put("/vector-stores/{vector_store_id}", responses=vector_store_responses) @authorize(Action.MANAGE_VECTOR_STORES) +@deprecated(VECTOR_STORES_DEPRECATED_REASON) async def update_vector_store( request: Request, vector_store_id: str, @@ -428,6 +445,7 @@ async def update_vector_store( responses=vector_store_delete_responses, ) @authorize(Action.MANAGE_VECTOR_STORES) +@deprecated(VECTOR_STORES_DEPRECATED_REASON) async def delete_vector_store( request: Request, vector_store_id: str, @@ -479,6 +497,7 @@ async def delete_vector_store( @router.post("/files", responses=file_responses) @authorize(Action.MANAGE_FILES) +@deprecated(VECTOR_STORES_DEPRECATED_REASON) async def create_file( # pylint: disable=too-many-branches,too-many-statements request: Request, auth: Annotated[AuthTuple, Depends(get_auth_dependency())], @@ -619,6 +638,7 @@ async def create_file( # pylint: disable=too-many-branches,too-many-statements "/vector-stores/{vector_store_id}/files", responses=vector_store_file_responses ) @authorize(Action.MANAGE_VECTOR_STORES) +@deprecated(VECTOR_STORES_DEPRECATED_REASON) async def add_file_to_vector_store( # pylint: disable=too-many-locals,too-many-statements,too-many-branches request: Request, vector_store_id: str, @@ -775,6 +795,7 @@ async def add_file_to_vector_store( # pylint: disable=too-many-locals,too-many- responses=vector_store_files_list_responses, ) @authorize(Action.READ_VECTOR_STORES) +@deprecated(VECTOR_STORES_DEPRECATED_REASON) async def list_vector_store_files( request: Request, vector_store_id: str, @@ -845,6 +866,7 @@ async def list_vector_store_files( responses=vector_store_file_responses, ) @authorize(Action.READ_VECTOR_STORES) +@deprecated(VECTOR_STORES_DEPRECATED_REASON) async def get_vector_store_file( request: Request, vector_store_id: str, @@ -916,6 +938,7 @@ async def get_vector_store_file( responses=vector_store_file_delete_responses, ) @authorize(Action.MANAGE_VECTOR_STORES) +@deprecated(VECTOR_STORES_DEPRECATED_REASON) async def delete_vector_store_file( request: Request, vector_store_id: str, diff --git a/src/app/main.py b/src/app/main.py index bafc74d63..da76d7c42 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -67,7 +67,13 @@ {"name": "streaming_query", "description": "Streaming query (SSE)."}, {"name": "streaming_query_interrupt", "description": "Streaming interrupt."}, {"name": "tools", "description": "Tools."}, - {"name": "vector-stores", "description": "Vector stores and files."}, + { + "name": "vector-stores", + "description": ( + "Vector stores and files (OGX proxy). Deprecated: will be removed in " + "the next LCS release." + ), + }, ] From 896b7aca94dfa691671a174d2a65a7e4bb364d42 Mon Sep 17 00:00:00 2001 From: Andrej Simurka Date: Mon, 14 Sep 2026 13:37:38 +0200 Subject: [PATCH 097/120] Applying explicit reasoning effort resolution in responses --- src/app/endpoints/responses.py | 12 ++-- src/utils/responses.py | 53 ++++++++++++++- tests/unit/utils/test_responses.py | 105 +++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+), 6 deletions(-) diff --git a/src/app/endpoints/responses.py b/src/app/endpoints/responses.py index 33dadc561..b1dc2a96f 100644 --- a/src/app/endpoints/responses.py +++ b/src/app/endpoints/responses.py @@ -93,6 +93,7 @@ ) from utils.quota_utils import check_tokens_available, get_available_quotas from utils.responses import ( + apply_reasoning_for_resolved_tools, build_tool_call_summary, build_turn_summary, check_model_configured, @@ -562,11 +563,6 @@ async def handle_responses_with_tracing( # pylint: disable=too-many-locals updated_request = responses_request.model_copy(deep=True) _ = responses_request - # Known LLS bug: https://redhat.atlassian.net/browse/LCORE-1583 - if original_request.reasoning is not None: - logger.warning("reasoning is not yet supported in LCORE and will be ignored") - updated_request.reasoning = None - check_configuration_loaded(configuration) started_at = datetime.now(UTC) rh_identity_context = get_rh_identity_context(request) @@ -668,6 +664,12 @@ async def handle_responses_with_tracing( # pylint: disable=too-many-locals mcp_headers, request.headers, ) + # Known LLS bug: https://redhat.atlassian.net/browse/LCORE-1583 + updated_request.reasoning = apply_reasoning_for_resolved_tools( + original_request.reasoning, + updated_request.tools, + updated_request.model, + ) # Extract vector store IDs for Inline RAG context from the original request vector_store_ids: Optional[list[str]] = ( diff --git a/src/utils/responses.py b/src/utils/responses.py index 7f856772d..ef9977c63 100644 --- a/src/utils/responses.py +++ b/src/utils/responses.py @@ -9,7 +9,10 @@ from fastapi import HTTPException from ogx_api import OpenAIResponseObject -from ogx_api.openai_responses import ApprovalFilter +from ogx_api.openai_responses import ( + ApprovalFilter, + OpenAIResponseReasoning, +) from ogx_api.openai_responses import ( OpenAIResponseContentPartRefusal as ContentPartRefusal, ) @@ -81,6 +84,7 @@ ) from ogx_client import ApiException, AsyncOgxClient from opentelemetry import trace +from pydantic_ai.profiles.openai import openai_model_profile import constants from configuration import configuration @@ -1826,6 +1830,53 @@ async def _resolve_server_tools( ) +def model_reasoning_enabled_by_default(model_id: str) -> bool: + """Return whether the model applies non-``none`` reasoning when omitted. + + Uses pydantic-ai's OpenAI model profile table (prefix-matched, live-verified). + Only these models need an explicit ``reasoning.effort: none`` when tools are + present on the OGX chat-completions path — e.g. gpt-5.6-terra, not gpt-4o-mini + or opt-in gpt-5.4. + """ + _, model_name = extract_provider_and_model_from_model_id(model_id) + profile = openai_model_profile(model_name) + return bool(profile.get("openai_reasoning_enabled_by_default", False)) + + +def apply_reasoning_for_resolved_tools( + reasoning: Optional[OpenAIResponseReasoning], + tools: Optional[list[InputTool]], + model_id: str, +) -> Optional[OpenAIResponseReasoning]: + """Set reasoning effort to ``none`` when resolved tools are present. + + Only applied when the model reasons by default if ``reasoning`` is omitted + (OpenAI gpt-5.6, gpt-5, o-series, etc.). Opt-in and non-reasoning models are + left unchanged. + + Args: + reasoning: Optional reasoning configuration to modify. + tools: Optional list of tools that have been resolved. + model_id: Resolved model identifier in ``provider/model`` form. + + Returns: + Modified reasoning configuration with effort set to "none" if tools + are present on a default-on reasoning model, or the original reasoning + otherwise. + """ + if not tools or not model_reasoning_enabled_by_default(model_id): + return reasoning + + base = reasoning or OpenAIResponseReasoning() + if base.effort is not None and base.effort != "none": + logger.warning( + "reasoning effort '%s' is not supported with tools in LCORE; " + "using effort='none'", + base.effort, + ) + return base.model_copy(update={"effort": "none"}) + + async def resolve_tool_choice( tools: Optional[list[InputTool]], tool_choice: Optional[ToolChoice], diff --git a/tests/unit/utils/test_responses.py b/tests/unit/utils/test_responses.py index ae9b98dac..093da7513 100644 --- a/tests/unit/utils/test_responses.py +++ b/tests/unit/utils/test_responses.py @@ -12,6 +12,7 @@ from ogx_api.openai_responses import ( AllowedToolsFilter, OpenAIResponseInputToolChoiceAllowedTools, + OpenAIResponseReasoning, ) from ogx_api.openai_responses import ApprovalFilter as OgxApprovalFilter from ogx_api.openai_responses import ( @@ -78,6 +79,7 @@ _build_chunk_attributes, _build_okp_doc_url, _merge_tools, + apply_reasoning_for_resolved_tools, build_mcp_tool_call_from_arguments_done, build_tool_call_summary, build_tool_result_from_mcp_output_item_done, @@ -92,6 +94,7 @@ get_topic_summary, is_server_deployed_output, maybe_get_topic_summary, + model_reasoning_enabled_by_default, parse_arguments_string, parse_referenced_documents, prepare_responses_params, @@ -1093,6 +1096,108 @@ async def test_disabled_emits_no_span( ] +class TestModelReasoningEnabledByDefault: + """Tests for model_reasoning_enabled_by_default.""" + + def test_gpt_5_6_default_on(self) -> None: + """gpt-5.6 reasons by default when reasoning is omitted.""" + assert model_reasoning_enabled_by_default("openai/gpt-5.6-terra") is True + + def test_o_series_default_on(self) -> None: + """o-series models reason by default.""" + assert model_reasoning_enabled_by_default("openai/o3-mini") is True + + def test_gpt_5_4_opt_in(self) -> None: + """gpt-5.4 defaults to reasoning off.""" + assert model_reasoning_enabled_by_default("openai/gpt-5.4") is False + + def test_gpt_5_chat_no_reasoning(self) -> None: + """gpt-5-chat does not use reasoning.""" + assert model_reasoning_enabled_by_default("openai/gpt-5-chat-latest") is False + + def test_gpt_4o_mini(self) -> None: + """gpt-4o-mini does not use reasoning.""" + assert model_reasoning_enabled_by_default("openai/gpt-4o-mini") is False + + +class TestApplyReasoningForResolvedTools: + """Tests for apply_reasoning_for_resolved_tools.""" + + _DEFAULT_ON_MODEL = "openai/gpt-5.6-terra" + _OPT_IN_MODEL = "openai/gpt-5.4" + _NON_REASONING_MODEL = "openai/gpt-4o-mini" + + @staticmethod + def _sample_tools() -> list[InputTool]: + return cast( + list[InputTool], + [InputToolFunction(name="lookup", parameters={"type": "object"})], + ) + + def test_preserves_reasoning_when_no_tools(self) -> None: + """Keep caller reasoning when the resolved request has no tools.""" + reasoning = OpenAIResponseReasoning(effort="high") + assert ( + apply_reasoning_for_resolved_tools(reasoning, None, self._DEFAULT_ON_MODEL) + == reasoning + ) + + def test_preserves_none_reasoning_when_no_tools(self) -> None: + """Leave reasoning unset when no tools are resolved.""" + assert ( + apply_reasoning_for_resolved_tools(None, None, self._DEFAULT_ON_MODEL) + is None + ) + + def test_skips_reasoning_for_non_reasoning_model_with_tools(self) -> None: + """Do not inject reasoning for models that do not use it.""" + assert ( + apply_reasoning_for_resolved_tools( + None, self._sample_tools(), self._NON_REASONING_MODEL + ) + is None + ) + + def test_skips_reasoning_for_opt_in_model_with_tools(self) -> None: + """Do not inject reasoning for opt-in models that default to off.""" + assert ( + apply_reasoning_for_resolved_tools( + None, self._sample_tools(), self._OPT_IN_MODEL + ) + is None + ) + + def test_sets_none_effort_when_tools_present_and_reasoning_missing(self) -> None: + """Default to explicit effort none when tools are present.""" + result = apply_reasoning_for_resolved_tools( + None, self._sample_tools(), self._DEFAULT_ON_MODEL + ) + assert result is not None + assert result.effort == "none" + + def test_forces_none_effort_when_tools_present_and_reasoning_requested( + self, + ) -> None: + """Override non-none reasoning effort when tools are present.""" + reasoning = OpenAIResponseReasoning(effort="medium") + result = apply_reasoning_for_resolved_tools( + reasoning, self._sample_tools(), self._DEFAULT_ON_MODEL + ) + assert result is not None + assert result.effort == "none" + + def test_keeps_none_effort_when_tools_present(self) -> None: + """Preserve other reasoning fields when forcing effort to none.""" + reasoning = OpenAIResponseReasoning(effort="none", summary="concise") + result = apply_reasoning_for_resolved_tools( + reasoning, self._sample_tools(), self._DEFAULT_ON_MODEL + ) + assert result is not None + assert result is not reasoning + assert result.effort == "none" + assert result.summary == "concise" + + class TestResolveToolChoice: """Tests for resolve_tool_choice (ToolChoiceMode, AllowedTools, explicit/implicit tools).""" From 3f3035afcc67d22c83b6c4df41a494f15dfbf856 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Mon, 14 Sep 2026 14:31:50 +0200 Subject: [PATCH 098/120] LCORE-4146: Updated model doc --- docs/models/common.puml | 202 ++-- docs/models/common.svg | 1150 +++++++++++----------- docs/models/requests.puml | 75 +- docs/models/requests.svg | 520 +++++----- docs/models/responses.puml | 200 ++-- docs/models/responses.svg | 1258 +++++++++++++------------ docs/models/successful_responses.json | 4 +- docs/models/successful_responses.md | 2 +- 8 files changed, 1730 insertions(+), 1681 deletions(-) diff --git a/docs/models/common.puml b/docs/models/common.puml index 39aaeccb8..2c424e48d 100644 --- a/docs/models/common.puml +++ b/docs/models/common.puml @@ -10,31 +10,31 @@ class "AgentTurnAccumulator" as src.models.common.agents.turn_accumulator.AgentT seen_docs : set[tuple[str, str]] text_parts : list[str] tool_round : int - turn_summary : TurnSummary + turn_summary vector_store_ids : Final[list[str]] increment_round_if_pending() -> None } class "Attachment" as src.models.common.query.Attachment { - attachment_type : Optional[str] - content : Optional[str] - content_type : Optional[str] + attachment_type : str + content : str + content_type : str model_config : dict validate_image_attachment() -> Self } class "CatalogModel" as src.models.common.models.CatalogModel { - api_model_type : Optional[str] - identifier : Optional[str] - metadata : Optional[dict[str, Any]] - model_type : Optional[str] - provider_id : Optional[str] - provider_resource_id : Optional[str] - type : Optional[str] + api_model_type : str + identifier : str + metadata : dict[str, Any] + model_type : str + provider_id : str + provider_resource_id : str + type : str } class "CatalogShield" as src.models.common.shields.CatalogShield { - config : Optional[dict[str, Any]] - name : Optional[str] - provider_id : Optional[Literal['question_validity', 'redaction']] - type : Optional[Literal['shield']] + config : dict[str, Any] + name : str + provider_id : Literal['question_validity', 'redaction'] + type : Literal['shield'] } class "CatalogTool" as src.models.common.tools.CatalogTool { description : str @@ -58,7 +58,7 @@ class "ConversationData" as src.models.common.conversation.ConversationData { topic_summary : Optional[str] } class "ConversationDetails" as src.models.common.conversation.ConversationDetails { - conversation_id : Optional[str] + conversation_id : str created_at : Optional[str] last_message_at : Optional[str] last_used_model : Optional[str] @@ -67,16 +67,16 @@ class "ConversationDetails" as src.models.common.conversation.ConversationDetail topic_summary : Optional[str] } class "ConversationTurn" as src.models.common.conversation.ConversationTurn { - completed_at : Optional[str] - messages : Optional[list[Message]] - model : Optional[str] - provider : Optional[str] - started_at : Optional[str] - tool_calls : Optional[list[ToolCallSummary]] - tool_results : Optional[list[ToolResultSummary]] + completed_at : str + messages : list[Message] + model : str + provider : str + started_at : str + tool_calls : list[ToolCallSummary] + tool_results : list[ToolResultSummary] } class "EndEventData" as src.models.common.agents.stream_payloads.EndEventData { - context_status : Literal + context_status input_tokens : int output_tokens : int referenced_documents : list[ReferencedDocument] @@ -124,39 +124,39 @@ class "ListedMcpTool" as src.models.common.tools.ListedMcpTool { name : str } class "MCPListToolsSummary" as src.models.common.turn_summary.MCPListToolsSummary { - server_label : Optional[str] - tools : Optional[list[ToolInfoSummary]] + server_label : str + tools : list[ToolInfoSummary] } class "MCPServerAuthInfo" as src.models.common.mcp.MCPServerAuthInfo { - client_auth_headers : Optional[list[str]] - name : Optional[str] + client_auth_headers : list[str] + name : str } class "MCPServerInfo" as src.models.common.mcp.MCPServerInfo { - name : Optional[str] - provider_id : Optional[str] - source : Optional[str] - url : Optional[str] + name : str + provider_id : str + source : str + url : str } class "Message" as src.models.common.conversation.Message { - content : Optional[str] + content : str referenced_documents : Optional[list[ReferencedDocument]] - type : Optional[Literal['user', 'assistant', 'system', 'developer']] + type : Literal['user', 'assistant', 'system', 'developer'] } class "ProviderHealthStatus" as src.models.common.health.ProviderHealthStatus { message : Optional[str] - provider_id : Optional[str] - status : Optional[str] + provider_id : str + status : str } class "RAGChunk" as src.models.common.turn_summary.RAGChunk { attributes : Optional[dict[str, Any]] - content : Optional[str] + content : str score : Optional[float] source : Optional[str] } class "RAGContext" as src.models.common.turn_summary.RAGContext { - context_text : Optional[str] - rag_chunks : Optional[list[RAGChunk]] - referenced_documents : Optional[list[ReferencedDocument]] + context_text : str + rag_chunks : list[RAGChunk] + referenced_documents : list[ReferencedDocument] } class "ReferencedDocument" as src.models.common.turn_summary.ReferencedDocument { doc_title : Optional[str] @@ -165,12 +165,12 @@ class "ReferencedDocument" as src.models.common.turn_summary.ReferencedDocument source : Optional[str] } class "ResponseGeneratorContext" as src.models.common.responses.contexts.ResponseGeneratorContext { - client : AsyncOgxClient + client conversation_id : str - inline_rag_context : RAGContext + inline_rag_context model_id : str moderation_result - query_request : QueryRequest + query_request rag_id_mapping : dict[str, str] request_id : str skip_userid_check : bool @@ -179,24 +179,24 @@ class "ResponseGeneratorContext" as src.models.common.responses.contexts.Respons vector_store_ids : list[str] } class "ResponsesApiParams" as src.models.common.responses.responses_api_params.ResponsesApiParams { - conversation : Optional[str] + conversation : str extra_headers : Optional[dict[str, str]] include : Optional[list[IncludeParameter]] - input : Optional[ResponseInput] + input instructions : Optional[str] max_infer_iters : Optional[int] max_output_tokens : Optional[int] max_tool_calls : Optional[int] metadata : Optional[dict[str, str]] - model : Optional[str] - omit_conversation : Optional[bool] + model : str + omit_conversation : bool parallel_tool_calls : Optional[bool] previous_response_id : Optional[str] prompt : Optional[Prompt] reasoning : Optional[Reasoning] safety_identifier : Optional[str] - store : Optional[bool] - stream : Optional[bool] + store : bool + stream : bool temperature : Optional[float] text : Optional[Text] tool_choice : Optional[ToolChoice] @@ -205,45 +205,45 @@ class "ResponsesApiParams" as src.models.common.responses.responses_api_params.R model_dump() -> dict[str, Any] } class "ResponsesContext" as src.models.common.responses.contexts.ResponsesContext { - auth : Optional[tuple[str, str, bool, str]] + auth : tuple[str, str, bool, str] background_tasks : Optional[BackgroundTasks] - client : Optional[AsyncOgxClient] + client compacted_original_input : Optional[ResponseInput] - endpoint_path : Optional[str] - filter_server_tools : Optional[bool] - generate_topic_summary : Optional[bool] - inline_rag_context : Optional[RAGContext] - input_text : Optional[str] - model_config : ConfigDict - moderation_result : Optional[ShieldModerationResult] - rh_identity_context : Optional[tuple[str, str]] - root_span : Span - started_at : Optional[datetime] + endpoint_path : str + filter_server_tools : bool + generate_topic_summary : bool + inline_rag_context + input_text : str + model_config + moderation_result + rh_identity_context : tuple[str, str] + root_span + started_at : datetime user_agent : Optional[str] } class "ResponsesConversationContext" as src.models.common.responses.responses_conversation_context.ResponsesConversationContext { - conversation : Optional[str] - generate_topic_summary : Optional[bool] - model_config : ConfigDict + conversation : str + generate_topic_summary : bool + model_config user_conversation : Optional[UserConversation] } class "ShieldModerationBlocked" as src.models.common.moderation.ShieldModerationBlocked { decision : Literal['blocked'] message : str moderation_id : str - refusal_response : ResponseMessage + refusal_response } class "ShieldModerationPassed" as src.models.common.moderation.ShieldModerationPassed { decision : Literal['passed'] } class "SkillMetadata" as src.models.common.skills.SkillMetadata { - description : Optional[str] - name : Optional[str] + description : str + name : str } class "SolrVectorSearchRequest" as src.models.common.query.SolrVectorSearchRequest { filters : Optional[dict[str, Any]] mode : Optional[Literal['semantic', 'hybrid', 'lexical', 'keyword']] - model_config : ConfigDict + model_config coerce_legacy_plain_dict(data: Any) -> Any } class "StartEventData" as src.models.common.agents.stream_payloads.StartEventData { @@ -256,7 +256,7 @@ class "StartStreamPayload" as src.models.common.agents.stream_payloads.StartStre create() -> Self } class "StreamPayloadBase" as src.models.common.agents.stream_payloads.StreamPayloadBase { - model_config : ConfigDict + model_config serialize_json() -> str serialize_text() -> str } @@ -271,42 +271,42 @@ class "TokenStreamPayload" as src.models.common.agents.stream_payloads.TokenStre serialize_text() -> str } class "ToolCallStreamPayload" as src.models.common.agents.stream_payloads.ToolCallStreamPayload { - data : ToolCallSummary + data event : Literal['tool_call'] serialize_text() -> str } class "ToolCallSummary" as src.models.common.turn_summary.ToolCallSummary { - args : Optional[dict[str, Any]] - id : Optional[str] - name : Optional[str] - type : Optional[str] + args : dict[str, Any] + id : str + name : str + type : str } class "ToolInfoSummary" as src.models.common.turn_summary.ToolInfoSummary { description : Optional[str] input_schema : Optional[dict[str, Any]] - name : Optional[str] + name : str } class "ToolResultStreamPayload" as src.models.common.agents.stream_payloads.ToolResultStreamPayload { - data : ToolResultSummary + data event : Literal['tool_result'] serialize_text() -> str } class "ToolResultSummary" as src.models.common.turn_summary.ToolResultSummary { - content : Optional[str] - id : Optional[str] - round : Optional[int] - status : Optional[str] - type : Optional[str] + content : str + id : str + round : int + status : str + type : str } class "Transcript" as src.models.common.transcripts.Transcript { - attachments : Optional[list[dict[str, Any]]] + attachments : list[dict[str, Any]] llm_response : str metadata query_is_valid : bool - rag_chunks : Optional[list[dict[str, Any]]] + rag_chunks : list[dict[str, Any]] redacted_query : str - tool_calls : Optional[list[dict[str, Any]]] - tool_results : Optional[list[dict[str, Any]]] + tool_calls : list[dict[str, Any]] + tool_results : list[dict[str, Any]] truncated : bool } class "TranscriptMetadata" as src.models.common.transcripts.TranscriptMetadata { @@ -324,16 +324,16 @@ class "TurnCompleteStreamPayload" as src.models.common.agents.stream_payloads.Tu create() -> Self } class "TurnSummary" as src.models.common.turn_summary.TurnSummary { - id : Optional[str] + id : str llm_response : str - next_chunk_id : Optional[int] - output_items : Optional[list[OpenAIResponseOutput]] - partial_tokens : Optional[list[str]] - rag_chunks : Optional[list[RAGChunk]] - referenced_documents : Optional[list[ReferencedDocument]] - token_usage : Optional[TokenCounter] - tool_calls : Optional[list[ToolCallSummary]] - tool_results : Optional[list[ToolResultSummary]] + next_chunk_id : int + output_items : list[OpenAIResponseOutput] + partial_tokens : list[str] + rag_chunks : list[RAGChunk] + referenced_documents : list[ReferencedDocument] + token_usage + tool_calls : list[ToolCallSummary] + tool_results : list[ToolResultSummary] } src.models.common.agents.stream_payloads.EndStreamPayload --|> src.models.common.agents.stream_payloads.StreamPayloadBase src.models.common.agents.stream_payloads.ErrorStreamPayload --|> src.models.common.agents.stream_payloads.StreamPayloadBase @@ -343,11 +343,11 @@ src.models.common.agents.stream_payloads.TokenStreamPayload --|> src.models.comm src.models.common.agents.stream_payloads.ToolCallStreamPayload --|> src.models.common.agents.stream_payloads.StreamPayloadBase src.models.common.agents.stream_payloads.ToolResultStreamPayload --|> src.models.common.agents.stream_payloads.StreamPayloadBase src.models.common.agents.stream_payloads.TurnCompleteStreamPayload --|> src.models.common.agents.stream_payloads.StreamPayloadBase -src.models.common.agents.stream_payloads.EndStreamPayload --> src.models.common.agents.stream_payloads.EndEventData : data -src.models.common.agents.stream_payloads.ErrorStreamPayload --> src.models.common.agents.stream_payloads.ErrorEventData : data -src.models.common.agents.stream_payloads.InterruptedStreamPayload --> src.models.common.agents.stream_payloads.InterruptedEventData : data -src.models.common.agents.stream_payloads.StartStreamPayload --> src.models.common.agents.stream_payloads.StartEventData : data -src.models.common.agents.stream_payloads.TokenStreamPayload --> src.models.common.agents.stream_payloads.TokenChunkData : data -src.models.common.agents.stream_payloads.TurnCompleteStreamPayload --> src.models.common.agents.stream_payloads.TokenChunkData : data -src.models.common.transcripts.Transcript --> src.models.common.transcripts.TranscriptMetadata : metadata +src.models.common.agents.stream_payloads.EndEventData --* src.models.common.agents.stream_payloads.EndStreamPayload : data +src.models.common.agents.stream_payloads.ErrorEventData --* src.models.common.agents.stream_payloads.ErrorStreamPayload : data +src.models.common.agents.stream_payloads.InterruptedEventData --* src.models.common.agents.stream_payloads.InterruptedStreamPayload : data +src.models.common.agents.stream_payloads.StartEventData --* src.models.common.agents.stream_payloads.StartStreamPayload : data +src.models.common.agents.stream_payloads.TokenChunkData --* src.models.common.agents.stream_payloads.TokenStreamPayload : data +src.models.common.agents.stream_payloads.TokenChunkData --* src.models.common.agents.stream_payloads.TurnCompleteStreamPayload : data +src.models.common.transcripts.TranscriptMetadata --* src.models.common.transcripts.Transcript : metadata @enduml diff --git a/docs/models/common.svg b/docs/models/common.svg index b4b5e2587..823c08fd1 100644 --- a/docs/models/common.svg +++ b/docs/models/common.svg @@ -1,5 +1,5 @@ - + @@ -18,750 +18,750 @@ seen_docs : set[tuple[str, str]] text_parts : list[str] tool_round : int - turn_summary : TurnSummary + turn_summary vector_store_ids : Final[list[str]] increment_round_if_pending() -> None - - - - Attachment - - attachment_type : Optional[str] - content : Optional[str] - content_type : Optional[str] - model_config : dict - - validate_image_attachment() -> Self + + + + Attachment + + attachment_type : str + content : str + content_type : str + model_config : dict + + validate_image_attachment() -> Self - - - - CatalogModel - - api_model_type : Optional[str] - identifier : Optional[str] - metadata : Optional[dict[str, Any]] - model_type : Optional[str] - provider_id : Optional[str] - provider_resource_id : Optional[str] - type : Optional[str] - + + + + CatalogModel + + api_model_type : str + identifier : str + metadata : dict[str, Any] + model_type : str + provider_id : str + provider_resource_id : str + type : str + - - - - CatalogShield - - config : Optional[dict[str, Any]] - name : Optional[str] - provider_id : Optional[Literal['question_validity', 'redaction']] - type : Optional[Literal['shield']] - + + + + CatalogShield + + config : dict[str, Any] + name : str + provider_id : Literal['question_validity', 'redaction'] + type : Literal['shield'] + - - - - CatalogTool - - description : str - identifier : str - parameters : list[CatalogToolParameter] - provider_id : str - server_source : str - toolgroup_id : str - type : str - + + + + CatalogTool + + description : str + identifier : str + parameters : list[CatalogToolParameter] + provider_id : str + server_source : str + toolgroup_id : str + type : str + - - - - CatalogToolParameter - - default : Optional[Any] - description : str - name : str - parameter_type : str - required : bool - + + + + CatalogToolParameter + + default : Optional[Any] + description : str + name : str + parameter_type : str + required : bool + - - - - ConversationData - - conversation_id : str - last_message_timestamp : float - topic_summary : Optional[str] - + + + + ConversationData + + conversation_id : str + last_message_timestamp : float + topic_summary : Optional[str] + - - - - ConversationDetails - - conversation_id : Optional[str] - created_at : Optional[str] - last_message_at : Optional[str] - last_used_model : Optional[str] - last_used_provider : Optional[str] - message_count : Optional[int] - topic_summary : Optional[str] - + + + + ConversationDetails + + conversation_id : str + created_at : Optional[str] + last_message_at : Optional[str] + last_used_model : Optional[str] + last_used_provider : Optional[str] + message_count : Optional[int] + topic_summary : Optional[str] + - - - - ConversationTurn - - completed_at : Optional[str] - messages : Optional[list[Message]] - model : Optional[str] - provider : Optional[str] - started_at : Optional[str] - tool_calls : Optional[list[ToolCallSummary]] - tool_results : Optional[list[ToolResultSummary]] - + + + + ConversationTurn + + completed_at : str + messages : list[Message] + model : str + provider : str + started_at : str + tool_calls : list[ToolCallSummary] + tool_results : list[ToolResultSummary] + - - - - EndEventData - - context_status : Literal - input_tokens : int - output_tokens : int - referenced_documents : list[ReferencedDocument] - truncated : Optional[bool] - + + + + EndEventData + + context_status + input_tokens : int + output_tokens : int + referenced_documents : list[ReferencedDocument] + truncated : Optional[bool] + - - - - EndStreamPayload - - available_quotas : dict[str, int] - data - event : Literal['end'] - - create() -> Self - serialize_text() -> str + + + + EndStreamPayload + + available_quotas : dict[str, int] + data + event : Literal['end'] + + create() -> Self + serialize_text() -> str - - - - ErrorEventData - - cause : str - response : str - status_code : int - + + + + ErrorEventData + + cause : str + response : str + status_code : int + - - - - ErrorStreamPayload - - data - event : Literal['error'] - - create() -> Self - from_error_response(error_response: AbstractErrorResponse) -> Self - serialize_text() -> str + + + + ErrorStreamPayload + + data + event : Literal['error'] + + create() -> Self + from_error_response(error_response: AbstractErrorResponse) -> Self + serialize_text() -> str - - - - FeedbackCategory - - name - + + + + FeedbackCategory + + name + - - - - HealthStatus - - name - + + + + HealthStatus + + name + - - - - InputToolMCP - - authorization : Optional[str] - + + + + InputToolMCP + + authorization : Optional[str] + - - - - InterruptedEventData - - request_id : str - + + + + InterruptedEventData + + request_id : str + - - - - InterruptedStreamPayload - - data - event : Literal['interrupted'] - - create() -> Self + + + + InterruptedStreamPayload + + data + event : Literal['interrupted'] + + create() -> Self - - - - ListedMcpTool - - description : Optional[str] - input_schema : Optional[dict[str, Any]] - name : str - + + + + ListedMcpTool + + description : Optional[str] + input_schema : Optional[dict[str, Any]] + name : str + - - - - MCPListToolsSummary - - server_label : Optional[str] - tools : Optional[list[ToolInfoSummary]] - + + + + MCPListToolsSummary + + server_label : str + tools : list[ToolInfoSummary] + - - - - MCPServerAuthInfo - - client_auth_headers : Optional[list[str]] - name : Optional[str] - + + + + MCPServerAuthInfo + + client_auth_headers : list[str] + name : str + - - - - MCPServerInfo - - name : Optional[str] - provider_id : Optional[str] - source : Optional[str] - url : Optional[str] - + + + + MCPServerInfo + + name : str + provider_id : str + source : str + url : str + - - - - Message - - content : Optional[str] - referenced_documents : Optional[list[ReferencedDocument]] - type : Optional[Literal['user', 'assistant', 'system', 'developer']] - + + + + Message + + content : str + referenced_documents : Optional[list[ReferencedDocument]] + type : Literal['user', 'assistant', 'system', 'developer'] + - - - - ProviderHealthStatus - - message : Optional[str] - provider_id : Optional[str] - status : Optional[str] - + + + + ProviderHealthStatus + + message : Optional[str] + provider_id : str + status : str + - - - - RAGChunk - - attributes : Optional[dict[str, Any]] - content : Optional[str] - score : Optional[float] - source : Optional[str] - + + + + RAGChunk + + attributes : Optional[dict[str, Any]] + content : str + score : Optional[float] + source : Optional[str] + - - - - RAGContext - - context_text : Optional[str] - rag_chunks : Optional[list[RAGChunk]] - referenced_documents : Optional[list[ReferencedDocument]] - + + + + RAGContext + + context_text : str + rag_chunks : list[RAGChunk] + referenced_documents : list[ReferencedDocument] + - - - - ReferencedDocument - - doc_title : Optional[str] - doc_url : Optional[AnyUrl] - document_id : Optional[str] - source : Optional[str] - + + + + ReferencedDocument + + doc_title : Optional[str] + doc_url : Optional[AnyUrl] + document_id : Optional[str] + source : Optional[str] + - - - - ResponseGeneratorContext - - client : AsyncOgxClient - conversation_id : str - inline_rag_context : RAGContext - model_id : str - moderation_result - query_request : QueryRequest - rag_id_mapping : dict[str, str] - request_id : str - skip_userid_check : bool - started_at : str - user_id : str - vector_store_ids : list[str] - + + + + ResponseGeneratorContext + + client + conversation_id : str + inline_rag_context + model_id : str + moderation_result + query_request + rag_id_mapping : dict[str, str] + request_id : str + skip_userid_check : bool + started_at : str + user_id : str + vector_store_ids : list[str] + - - - - ResponsesApiParams - - conversation : Optional[str] - extra_headers : Optional[dict[str, str]] - include : Optional[list[IncludeParameter]] - input : Optional[ResponseInput] - instructions : Optional[str] - max_infer_iters : Optional[int] - max_output_tokens : Optional[int] - max_tool_calls : Optional[int] - metadata : Optional[dict[str, str]] - model : Optional[str] - omit_conversation : Optional[bool] - parallel_tool_calls : Optional[bool] - previous_response_id : Optional[str] - prompt : Optional[Prompt] - reasoning : Optional[Reasoning] - safety_identifier : Optional[str] - store : Optional[bool] - stream : Optional[bool] - temperature : Optional[float] - text : Optional[Text] - tool_choice : Optional[ToolChoice] - tools : Optional[list[InputTool]] - - echoed_params(rag_id_mapping: Mapping[str, str]) -> dict[str, Any] - model_dump() -> dict[str, Any] + + + + ResponsesApiParams + + conversation : str + extra_headers : Optional[dict[str, str]] + include : Optional[list[IncludeParameter]] + input + instructions : Optional[str] + max_infer_iters : Optional[int] + max_output_tokens : Optional[int] + max_tool_calls : Optional[int] + metadata : Optional[dict[str, str]] + model : str + omit_conversation : bool + parallel_tool_calls : Optional[bool] + previous_response_id : Optional[str] + prompt : Optional[Prompt] + reasoning : Optional[Reasoning] + safety_identifier : Optional[str] + store : bool + stream : bool + temperature : Optional[float] + text : Optional[Text] + tool_choice : Optional[ToolChoice] + tools : Optional[list[InputTool]] + + echoed_params(rag_id_mapping: Mapping[str, str]) -> dict[str, Any] + model_dump() -> dict[str, Any] - - - - ResponsesContext - - auth : Optional[tuple[str, str, bool, str]] - background_tasks : Optional[BackgroundTasks] - client : Optional[AsyncOgxClient] - compacted_original_input : Optional[ResponseInput] - endpoint_path : Optional[str] - filter_server_tools : Optional[bool] - generate_topic_summary : Optional[bool] - inline_rag_context : Optional[RAGContext] - input_text : Optional[str] - model_config : ConfigDict - moderation_result : Optional[ShieldModerationResult] - rh_identity_context : Optional[tuple[str, str]] - root_span : Span - started_at : Optional[datetime] - user_agent : Optional[str] - + + + + ResponsesContext + + auth : tuple[str, str, bool, str] + background_tasks : Optional[BackgroundTasks] + client + compacted_original_input : Optional[ResponseInput] + endpoint_path : str + filter_server_tools : bool + generate_topic_summary : bool + inline_rag_context + input_text : str + model_config + moderation_result + rh_identity_context : tuple[str, str] + root_span + started_at : datetime + user_agent : Optional[str] + - - - - ResponsesConversationContext - - conversation : Optional[str] - generate_topic_summary : Optional[bool] - model_config : ConfigDict - user_conversation : Optional[UserConversation] - + + + + ResponsesConversationContext + + conversation : str + generate_topic_summary : bool + model_config + user_conversation : Optional[UserConversation] + - - - - ShieldModerationBlocked - - decision : Literal['blocked'] - message : str - moderation_id : str - refusal_response : ResponseMessage - + + + + ShieldModerationBlocked + + decision : Literal['blocked'] + message : str + moderation_id : str + refusal_response + - - - - ShieldModerationPassed - - decision : Literal['passed'] - + + + + ShieldModerationPassed + + decision : Literal['passed'] + - - - - SkillMetadata - - description : Optional[str] - name : Optional[str] - + + + + SkillMetadata + + description : str + name : str + - - - - SolrVectorSearchRequest - - filters : Optional[dict[str, Any]] - mode : Optional[Literal['semantic', 'hybrid', 'lexical', 'keyword']] - model_config : ConfigDict - - coerce_legacy_plain_dict(data: Any) -> Any + + + + SolrVectorSearchRequest + + filters : Optional[dict[str, Any]] + mode : Optional[Literal['semantic', 'hybrid', 'lexical', 'keyword']] + model_config + + coerce_legacy_plain_dict(data: Any) -> Any - - - - StartEventData - - conversation_id : str - request_id : str - + + + + StartEventData + + conversation_id : str + request_id : str + - - - - StartStreamPayload - - data - event : Literal['start'] - - create() -> Self + + + + StartStreamPayload + + data + event : Literal['start'] + + create() -> Self - - - - StreamPayloadBase - - model_config : ConfigDict - - serialize_json() -> str - serialize_text() -> str + + + + StreamPayloadBase + + model_config + + serialize_json() -> str + serialize_text() -> str - - - - TokenChunkData - - id : int - token : str - + + + + TokenChunkData + + id : int + token : str + - - - - TokenStreamPayload - - data - event : Literal['token'] - - create() -> Self - serialize_text() -> str + + + + TokenStreamPayload + + data + event : Literal['token'] + + create() -> Self + serialize_text() -> str - - - - ToolCallStreamPayload - - data : ToolCallSummary - event : Literal['tool_call'] - - serialize_text() -> str + + + + ToolCallStreamPayload + + data + event : Literal['tool_call'] + + serialize_text() -> str - - - - ToolCallSummary - - args : Optional[dict[str, Any]] - id : Optional[str] - name : Optional[str] - type : Optional[str] - + + + + ToolCallSummary + + args : dict[str, Any] + id : str + name : str + type : str + - - - - ToolInfoSummary - - description : Optional[str] - input_schema : Optional[dict[str, Any]] - name : Optional[str] - + + + + ToolInfoSummary + + description : Optional[str] + input_schema : Optional[dict[str, Any]] + name : str + - - - - ToolResultStreamPayload - - data : ToolResultSummary - event : Literal['tool_result'] - - serialize_text() -> str + + + + ToolResultStreamPayload + + data + event : Literal['tool_result'] + + serialize_text() -> str - - - - ToolResultSummary - - content : Optional[str] - id : Optional[str] - round : Optional[int] - status : Optional[str] - type : Optional[str] - + + + + ToolResultSummary + + content : str + id : str + round : int + status : str + type : str + - - - - Transcript - - attachments : Optional[list[dict[str, Any]]] - llm_response : str - metadata - query_is_valid : bool - rag_chunks : Optional[list[dict[str, Any]]] - redacted_query : str - tool_calls : Optional[list[dict[str, Any]]] - tool_results : Optional[list[dict[str, Any]]] - truncated : bool - + + + + Transcript + + attachments : list[dict[str, Any]] + llm_response : str + metadata + query_is_valid : bool + rag_chunks : list[dict[str, Any]] + redacted_query : str + tool_calls : list[dict[str, Any]] + tool_results : list[dict[str, Any]] + truncated : bool + - - - - TranscriptMetadata - - conversation_id : str - model : str - provider : Optional[str] - query_model : Optional[str] - query_provider : Optional[str] - timestamp : str - user_id : str - + + + + TranscriptMetadata + + conversation_id : str + model : str + provider : Optional[str] + query_model : Optional[str] + query_provider : Optional[str] + timestamp : str + user_id : str + - - - - TurnCompleteStreamPayload - - data - event : Literal['turn_complete'] - - create() -> Self + + + + TurnCompleteStreamPayload + + data + event : Literal['turn_complete'] + + create() -> Self - - - - TurnSummary - - id : Optional[str] - llm_response : str - next_chunk_id : Optional[int] - output_items : Optional[list[OpenAIResponseOutput]] - partial_tokens : Optional[list[str]] - rag_chunks : Optional[list[RAGChunk]] - referenced_documents : Optional[list[ReferencedDocument]] - token_usage : Optional[TokenCounter] - tool_calls : Optional[list[ToolCallSummary]] - tool_results : Optional[list[ToolResultSummary]] - + + + + TurnSummary + + id : str + llm_response : str + next_chunk_id : int + output_items : list[OpenAIResponseOutput] + partial_tokens : list[str] + rag_chunks : list[RAGChunk] + referenced_documents : list[ReferencedDocument] + token_usage + tool_calls : list[ToolCallSummary] + tool_results : list[ToolResultSummary] + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - - - data - - - - - - data - - - - - - data - - - - - - data - - - - - - data - - - - - - data - - - - - - metadata - - + + + + + + + + data + + + + + + data + + + + + + data + + + + + + data + + + + + + data + + + + + + data + + + + + + metadata + + diff --git a/docs/models/requests.puml b/docs/models/requests.puml index 99f8b7012..904a8e6f4 100644 --- a/docs/models/requests.puml +++ b/docs/models/requests.puml @@ -2,16 +2,16 @@ set namespaceSeparator none class "ConversationUpdateRequest" as src.models.api.requests.conversations.ConversationUpdateRequest { model_config : dict - topic_summary : Optional[str] + topic_summary : str } class "FeedbackRequest" as src.models.api.requests.feedback.FeedbackRequest { categories : Optional[list[FeedbackCategory]] - conversation_id : Optional[str] - llm_response : Optional[str] + conversation_id : str + llm_response : str model_config : dict sentiment : Optional[int] user_feedback : Optional[str] - user_question : Optional[str] + user_question : str check_feedback_provided() -> Self check_sentiment(value: Optional[int]) -> Optional[int] check_uuid(value: str) -> str @@ -19,17 +19,17 @@ class "FeedbackRequest" as src.models.api.requests.feedback.FeedbackRequest { } class "FeedbackStatusUpdateRequest" as src.models.api.requests.feedback.FeedbackStatusUpdateRequest { model_config : dict - status : Optional[bool] + status : bool get_value() -> bool } class "MCPServerRegistrationRequest" as src.models.api.requests.mcp_servers.MCPServerRegistrationRequest { authorization_headers : Optional[dict[str, str]] headers : Optional[list[str]] model_config : dict - name : Optional[str] - provider_id : Optional[str] + name : str + provider_id : str timeout : Optional[int] - url : Optional[str] + url : str validate_authorization_header_values(value: Optional[dict[str, str]]) -> Optional[dict[str, str]] validate_url(value: str) -> str } @@ -39,15 +39,15 @@ class "ModelFilter" as src.models.api.requests.catalog.ModelFilter { } class "PromptCreateRequest" as src.models.api.requests.prompts.PromptCreateRequest { model_config : dict - prompt : Optional[str] + prompt : str variables : Optional[list[str]] } class "PromptUpdateRequest" as src.models.api.requests.prompts.PromptUpdateRequest { model_config : dict - prompt : Optional[str] + prompt : str set_as_default : Optional[bool] variables : Optional[list[str]] - version : Optional[int] + version : int } class "QueryRequest" as src.models.api.requests.query.QueryRequest { attachments : Optional[list[Attachment]] @@ -58,7 +58,7 @@ class "QueryRequest" as src.models.api.requests.query.QueryRequest { model_config : dict no_tools : Optional[bool] provider : Optional[str] - query : Optional[str] + query : str shield_ids : Optional[list[str]] solr : Optional[SolrVectorSearchRequest] system_prompt : Optional[str] @@ -98,46 +98,46 @@ class "ResponsesRequest" as src.models.api.requests.responses_openai.ResponsesRe validate_conversation_and_previous_response_id_mutually_exclusive() -> Self } class "RlsapiV1Attachment" as src.models.api.requests.rlsapi.RlsapiV1Attachment { - contents : Optional[str] - mimetype : Optional[str] + contents : str + mimetype : str } class "RlsapiV1CLA" as src.models.api.requests.rlsapi.RlsapiV1CLA { - nevra : Optional[str] - version : Optional[str] + nevra : str + version : str } class "RlsapiV1Context" as src.models.api.requests.rlsapi.RlsapiV1Context { - attachments : Optional[RlsapiV1Attachment] - cla : Optional[RlsapiV1CLA] - stdin : Optional[str] - systeminfo : Optional[RlsapiV1SystemInfo] - terminal : Optional[RlsapiV1Terminal] + attachments + cla + stdin : str + systeminfo + terminal } class "RlsapiV1InferRequest" as src.models.api.requests.rlsapi.RlsapiV1InferRequest { - context : Optional[RlsapiV1Context] - include_metadata : Optional[bool] - question : Optional[str] - skip_rag : Optional[bool] + context + include_metadata : bool + question : str + skip_rag : bool get_input_source() -> str validate_question(value: str) -> str } class "RlsapiV1SystemInfo" as src.models.api.requests.rlsapi.RlsapiV1SystemInfo { - arch : Optional[str] + arch : str model_config : dict - os : Optional[str] - system_id : Optional[str] - version : Optional[str] + os : str + system_id : str + version : str } class "RlsapiV1Terminal" as src.models.api.requests.rlsapi.RlsapiV1Terminal { - output : Optional[str] + output : str } class "SavedPromptCreateRequest" as src.models.api.requests.saved_prompts.SavedPromptCreateRequest { - content : Optional[str] + content : str model_config : dict - name : Optional[str] + name : str } class "StreamingInterruptRequest" as src.models.api.requests.query.StreamingInterruptRequest { model_config : dict - request_id : Optional[str] + request_id : str check_request_id(value: str) -> str } class "VectorStoreCreateRequest" as src.models.api.requests.vector_stores.VectorStoreCreateRequest { @@ -146,13 +146,13 @@ class "VectorStoreCreateRequest" as src.models.api.requests.vector_stores.Vector embedding_model : Optional[str] metadata : Optional[dict[str, Any]] model_config : dict - name : Optional[str] + name : str provider_id : Optional[str] } class "VectorStoreFileCreateRequest" as src.models.api.requests.vector_stores.VectorStoreFileCreateRequest { attributes : Optional[dict[str, str | float | bool]] chunking_strategy : Optional[dict[str, Any]] - file_id : Optional[str] + file_id : str model_config : dict validate_attributes(value: Optional[dict[str, str | float | bool]]) -> Optional[dict[str, str | float | bool]] } @@ -163,4 +163,9 @@ class "VectorStoreUpdateRequest" as src.models.api.requests.vector_stores.Vector name : Optional[str] check_at_least_one_field() -> Self } +src.models.api.requests.rlsapi.RlsapiV1Attachment --* src.models.api.requests.rlsapi.RlsapiV1Context : attachments +src.models.api.requests.rlsapi.RlsapiV1CLA --* src.models.api.requests.rlsapi.RlsapiV1Context : cla +src.models.api.requests.rlsapi.RlsapiV1Context --* src.models.api.requests.rlsapi.RlsapiV1InferRequest : context +src.models.api.requests.rlsapi.RlsapiV1SystemInfo --* src.models.api.requests.rlsapi.RlsapiV1Context : systeminfo +src.models.api.requests.rlsapi.RlsapiV1Terminal --* src.models.api.requests.rlsapi.RlsapiV1Context : terminal @enduml diff --git a/docs/models/requests.svg b/docs/models/requests.svg index d11285e1d..9ae9f16f1 100644 --- a/docs/models/requests.svg +++ b/docs/models/requests.svg @@ -1,310 +1,340 @@ - + - - - - ConversationUpdateRequest - - model_config : dict - topic_summary : Optional[str] - + + + + ConversationUpdateRequest + + model_config : dict + topic_summary : str + - - - - FeedbackRequest - - categories : Optional[list[FeedbackCategory]] - conversation_id : Optional[str] - llm_response : Optional[str] - model_config : dict - sentiment : Optional[int] - user_feedback : Optional[str] - user_question : Optional[str] - - check_feedback_provided() -> Self - check_sentiment(value: Optional[int]) -> Optional[int] - check_uuid(value: str) -> str - validate_categories(value: Optional[list[FeedbackCategory]]) -> Optional[list[FeedbackCategory]] + + + + FeedbackRequest + + categories : Optional[list[FeedbackCategory]] + conversation_id : str + llm_response : str + model_config : dict + sentiment : Optional[int] + user_feedback : Optional[str] + user_question : str + + check_feedback_provided() -> Self + check_sentiment(value: Optional[int]) -> Optional[int] + check_uuid(value: str) -> str + validate_categories(value: Optional[list[FeedbackCategory]]) -> Optional[list[FeedbackCategory]] - - - - FeedbackStatusUpdateRequest - - model_config : dict - status : Optional[bool] - - get_value() -> bool + + + + FeedbackStatusUpdateRequest + + model_config : dict + status : bool + + get_value() -> bool - - - - MCPServerRegistrationRequest - - authorization_headers : Optional[dict[str, str]] - headers : Optional[list[str]] - model_config : dict - name : Optional[str] - provider_id : Optional[str] - timeout : Optional[int] - url : Optional[str] - - validate_authorization_header_values(value: Optional[dict[str, str]]) -> Optional[dict[str, str]] - validate_url(value: str) -> str + + + + MCPServerRegistrationRequest + + authorization_headers : Optional[dict[str, str]] + headers : Optional[list[str]] + model_config : dict + name : str + provider_id : str + timeout : Optional[int] + url : str + + validate_authorization_header_values(value: Optional[dict[str, str]]) -> Optional[dict[str, str]] + validate_url(value: str) -> str - - - - ModelFilter - - model_config : dict - model_type : Optional[str] - + + + + ModelFilter + + model_config : dict + model_type : Optional[str] + - - - - PromptCreateRequest - - model_config : dict - prompt : Optional[str] - variables : Optional[list[str]] - + + + + PromptCreateRequest + + model_config : dict + prompt : str + variables : Optional[list[str]] + - - - - PromptUpdateRequest - - model_config : dict - prompt : Optional[str] - set_as_default : Optional[bool] - variables : Optional[list[str]] - version : Optional[int] - + + + + PromptUpdateRequest + + model_config : dict + prompt : str + set_as_default : Optional[bool] + variables : Optional[list[str]] + version : int + - - - - QueryRequest - - attachments : Optional[list[Attachment]] - conversation_id : Optional[str] - generate_topic_summary : Optional[bool] - media_type : Optional[str] - model : Optional[str] - model_config : dict - no_tools : Optional[bool] - provider : Optional[str] - query : Optional[str] - shield_ids : Optional[list[str]] - solr : Optional[SolrVectorSearchRequest] - system_prompt : Optional[str] - vector_store_ids : Optional[list[str]] - - check_uuid(value: Optional[str]) -> Optional[str] - validate_media_type() -> Self - validate_provider_and_model() -> Self + + + + QueryRequest + + attachments : Optional[list[Attachment]] + conversation_id : Optional[str] + generate_topic_summary : Optional[bool] + media_type : Optional[str] + model : Optional[str] + model_config : dict + no_tools : Optional[bool] + provider : Optional[str] + query : str + shield_ids : Optional[list[str]] + solr : Optional[SolrVectorSearchRequest] + system_prompt : Optional[str] + vector_store_ids : Optional[list[str]] + + check_uuid(value: Optional[str]) -> Optional[str] + validate_media_type() -> Self + validate_provider_and_model() -> Self - - - - ResponsesRequest - - conversation : Optional[str] - generate_topic_summary : Optional[bool] - include : Optional[list[IncludeParameter]] - input - instructions : Optional[str] - max_infer_iters : Optional[int] - max_output_tokens : Optional[int] - max_tool_calls : Optional[int] - metadata : Optional[dict[str, str]] - model : Optional[str] - model_config : dict - parallel_tool_calls : Optional[bool] - previous_response_id : Optional[str] - prompt : Optional[Prompt] - reasoning : Optional[Reasoning] - safety_identifier : Optional[str] - shield_ids : Optional[list[str]] - solr : Optional[SolrVectorSearchRequest] - store : bool - stream : bool - temperature : Optional[float] - text : Optional[Text] - tool_choice : Optional[ToolChoice] - tools : Optional[list[InputTool]] - - check_previous_response_id(value: Optional[str]) -> Optional[str] - check_suid(value: Optional[str]) -> Optional[str] - validate_body_size(values: Any) -> Any - validate_conversation_and_previous_response_id_mutually_exclusive() -> Self + + + + ResponsesRequest + + conversation : Optional[str] + generate_topic_summary : Optional[bool] + include : Optional[list[IncludeParameter]] + input + instructions : Optional[str] + max_infer_iters : Optional[int] + max_output_tokens : Optional[int] + max_tool_calls : Optional[int] + metadata : Optional[dict[str, str]] + model : Optional[str] + model_config : dict + parallel_tool_calls : Optional[bool] + previous_response_id : Optional[str] + prompt : Optional[Prompt] + reasoning : Optional[Reasoning] + safety_identifier : Optional[str] + shield_ids : Optional[list[str]] + solr : Optional[SolrVectorSearchRequest] + store : bool + stream : bool + temperature : Optional[float] + text : Optional[Text] + tool_choice : Optional[ToolChoice] + tools : Optional[list[InputTool]] + + check_previous_response_id(value: Optional[str]) -> Optional[str] + check_suid(value: Optional[str]) -> Optional[str] + validate_body_size(values: Any) -> Any + validate_conversation_and_previous_response_id_mutually_exclusive() -> Self - - - - RlsapiV1Attachment - - contents : Optional[str] - mimetype : Optional[str] - + + + + RlsapiV1Attachment + + contents : str + mimetype : str + - - - - RlsapiV1CLA - - nevra : Optional[str] - version : Optional[str] - + + + + RlsapiV1CLA + + nevra : str + version : str + - - - - RlsapiV1Context - - attachments : Optional[RlsapiV1Attachment] - cla : Optional[RlsapiV1CLA] - stdin : Optional[str] - systeminfo : Optional[RlsapiV1SystemInfo] - terminal : Optional[RlsapiV1Terminal] - + + + + RlsapiV1Context + + attachments + cla + stdin : str + systeminfo + terminal + - - - - RlsapiV1InferRequest - - context : Optional[RlsapiV1Context] - include_metadata : Optional[bool] - question : Optional[str] - skip_rag : Optional[bool] - - get_input_source() -> str - validate_question(value: str) -> str + + + + RlsapiV1InferRequest + + context + include_metadata : bool + question : str + skip_rag : bool + + get_input_source() -> str + validate_question(value: str) -> str - - - - RlsapiV1SystemInfo - - arch : Optional[str] - model_config : dict - os : Optional[str] - system_id : Optional[str] - version : Optional[str] - + + + + RlsapiV1SystemInfo + + arch : str + model_config : dict + os : str + system_id : str + version : str + - - - - RlsapiV1Terminal - - output : Optional[str] - + + + + RlsapiV1Terminal + + output : str + - - - - SavedPromptCreateRequest - - content : Optional[str] - model_config : dict - name : Optional[str] - + + + + SavedPromptCreateRequest + + content : str + model_config : dict + name : str + - - - - StreamingInterruptRequest - - model_config : dict - request_id : Optional[str] - - check_request_id(value: str) -> str + + + + StreamingInterruptRequest + + model_config : dict + request_id : str + + check_request_id(value: str) -> str - - - - VectorStoreCreateRequest - - chunking_strategy : Optional[dict[str, Any]] - embedding_dimension : Optional[int] - embedding_model : Optional[str] - metadata : Optional[dict[str, Any]] - model_config : dict - name : Optional[str] - provider_id : Optional[str] - + + + + VectorStoreCreateRequest + + chunking_strategy : Optional[dict[str, Any]] + embedding_dimension : Optional[int] + embedding_model : Optional[str] + metadata : Optional[dict[str, Any]] + model_config : dict + name : str + provider_id : Optional[str] + - - - - VectorStoreFileCreateRequest - - attributes : Optional[dict[str, str | float | bool]] - chunking_strategy : Optional[dict[str, Any]] - file_id : Optional[str] - model_config : dict - - validate_attributes(value: Optional[dict[str, str | float | bool]]) -> Optional[dict[str, str | float | bool]] + + + + VectorStoreFileCreateRequest + + attributes : Optional[dict[str, str | float | bool]] + chunking_strategy : Optional[dict[str, Any]] + file_id : str + model_config : dict + + validate_attributes(value: Optional[dict[str, str | float | bool]]) -> Optional[dict[str, str | float | bool]] - - - - VectorStoreUpdateRequest - - expires_at : Optional[int] - metadata : Optional[dict[str, Any]] - model_config : dict - name : Optional[str] - - check_at_least_one_field() -> Self + + + + VectorStoreUpdateRequest + + expires_at : Optional[int] + metadata : Optional[dict[str, Any]] + model_config : dict + name : Optional[str] + + check_at_least_one_field() -> Self - + + + + + attachments + + + + + + cla + + + + + + context + + + + + + systeminfo + + + + + + terminal + + diff --git a/docs/models/responses.puml b/docs/models/responses.puml index eb069dbf6..0eef6a549 100644 --- a/docs/models/responses.puml +++ b/docs/models/responses.puml @@ -1,14 +1,14 @@ @startuml classes set namespaceSeparator none class "AbstractDeleteResponse" as src.models.api.responses.successful.bases.AbstractDeleteResponse { - deleted : Optional[bool] + deleted : bool resource_name : ClassVar[str] openapi_response() -> dict[str, Any] response() -> str } class "AbstractErrorResponse" as src.models.api.responses.error.bases.AbstractErrorResponse { - detail : Optional[DetailModel] - status_code : Optional[int] + detail + status_code : int get_description() -> str openapi_response(examples: Optional[list[str]]) -> dict[str, Any] } @@ -17,16 +17,16 @@ class "AbstractSuccessfulResponse" as src.models.api.responses.successful.bases. } class "AuthorizedResponse" as src.models.api.responses.successful.probes.AuthorizedResponse { model_config : dict - skip_userid_check : Optional[bool] - user_id : Optional[str] - username : Optional[str] + skip_userid_check : bool + user_id : str + username : str } class "BadRequestResponse" as src.models.api.responses.error.bad_request.BadRequestResponse { description : ClassVar[str] model_config : dict } class "ConfigurationResponse" as src.models.api.responses.successful.configuration.ConfigurationResponse { - configuration : Configuration + configuration model_config : ConfigDict } class "ConflictResponse" as src.models.api.responses.error.conflict.ConflictResponse { @@ -36,21 +36,21 @@ class "ConflictResponse" as src.models.api.responses.error.conflict.ConflictResp mcp_tool(server_label: str) -> Self } class "ConversationDeleteResponse" as src.models.api.responses.successful.conversations.ConversationDeleteResponse { - conversation_id : Optional[str] + conversation_id : str model_config : dict resource_name : ClassVar[str] success() -> bool } class "ConversationResponse" as src.models.api.responses.successful.conversations.ConversationResponse { - chat_history : Optional[list[ConversationTurn]] - conversation_id : Optional[str] + chat_history : list[ConversationTurn] + conversation_id : str model_config : dict } class "ConversationUpdateResponse" as src.models.api.responses.successful.conversations.ConversationUpdateResponse { - conversation_id : Optional[str] - message : Optional[str] + conversation_id : str + message : str model_config : dict - success : Optional[bool] + success : bool } class "ConversationsListResponse" as src.models.api.responses.successful.conversations.ConversationsListResponse { conversations : list[ConversationDetails] @@ -61,25 +61,25 @@ class "ConversationsListResponseV2" as src.models.api.responses.successful.conve model_config : dict } class "DetailModel" as src.models.api.responses.error.bases.DetailModel { - cause : Optional[str] - response : Optional[str] + cause : str + response : str } class "FeedbackResponse" as src.models.api.responses.successful.feedback.FeedbackResponse { model_config : dict - response : Optional[str] + response : str } class "FeedbackStatusUpdateResponse" as src.models.api.responses.successful.feedback.FeedbackStatusUpdateResponse { model_config : dict status : dict[str, Any] } class "FileResponse" as src.models.api.responses.successful.vector_stores.FileResponse { - bytes : Optional[int] - created_at : Optional[int] - filename : Optional[str] - id : Optional[str] + bytes : int + created_at : int + filename : str + id : str model_config : dict - object : Optional[str] - purpose : Optional[str] + object : str + purpose : str } class "FileTooLargeResponse" as src.models.api.responses.error.content_too_large.FileTooLargeResponse { description : ClassVar[str] @@ -99,9 +99,9 @@ class "ForbiddenResponse" as src.models.api.responses.error.forbidden.ForbiddenR } class "InfoResponse" as src.models.api.responses.successful.probes.InfoResponse { model_config : dict - name : Optional[str] - ogx_version : Optional[str] - service_version : Optional[str] + name : str + ogx_version : str + service_version : str } class "InternalServerErrorResponse" as src.models.api.responses.error.internal.InternalServerErrorResponse { description : ClassVar[str] @@ -115,32 +115,32 @@ class "InternalServerErrorResponse" as src.models.api.responses.error.internal.I query_failed(cause: str) -> Self } class "LivenessResponse" as src.models.api.responses.successful.probes.LivenessResponse { - alive : Optional[bool] + alive : bool model_config : dict } class "MCPClientAuthOptionsResponse" as src.models.api.responses.successful.mcp_servers.MCPClientAuthOptionsResponse { model_config : dict - servers : Optional[list[MCPServerAuthInfo]] + servers : list[MCPServerAuthInfo] } class "MCPServerDeleteResponse" as src.models.api.responses.successful.mcp_servers.MCPServerDeleteResponse { model_config : dict - name : Optional[str] + name : str resource_name : ClassVar[str] } class "MCPServerListResponse" as src.models.api.responses.successful.mcp_servers.MCPServerListResponse { model_config : dict - servers : Optional[list[MCPServerInfo]] + servers : list[MCPServerInfo] } class "MCPServerRegistrationResponse" as src.models.api.responses.successful.mcp_servers.MCPServerRegistrationResponse { - message : Optional[str] + message : str model_config : dict - name : Optional[str] - provider_id : Optional[str] - url : Optional[str] + name : str + provider_id : str + url : str } class "ModelsResponse" as src.models.api.responses.successful.catalog.ModelsResponse { model_config : dict - models : Optional[list[CatalogModel]] + models : list[CatalogModel] } class "NotFoundResponse" as src.models.api.responses.error.not_found.NotFoundResponse { description : ClassVar[str] @@ -148,50 +148,50 @@ class "NotFoundResponse" as src.models.api.responses.error.not_found.NotFoundRes } class "PromptDeleteResponse" as src.models.api.responses.successful.prompts.PromptDeleteResponse { model_config : dict - prompt_id : Optional[str] + prompt_id : str resource_name : ClassVar[str] } class "PromptResourceResponse" as src.models.api.responses.successful.prompts.PromptResourceResponse { is_default : Optional[bool] model_config : dict prompt : Optional[str] - prompt_id : Optional[str] + prompt_id : str variables : Optional[list[str]] - version : Optional[int] + version : int } class "PromptTooLongResponse" as src.models.api.responses.error.content_too_large.PromptTooLongResponse { description : ClassVar[str] model_config : dict } class "PromptsListResponse" as src.models.api.responses.successful.prompts.PromptsListResponse { - data : Optional[list[PromptResourceResponse]] + data : list[PromptResourceResponse] model_config : dict } class "ProviderResponse" as src.models.api.responses.successful.catalog.ProviderResponse { - api : Optional[str] - config : Optional[dict[str, Any]] - health : Optional[dict[str, Any]] + api : str + config : dict[str, Any] + health : dict[str, Any] model_config : dict - provider_id : Optional[str] - provider_type : Optional[str] + provider_id : str + provider_type : str } class "ProvidersListResponse" as src.models.api.responses.successful.catalog.ProvidersListResponse { model_config : dict - providers : Optional[dict[str, list[dict[str, Any]]]] + providers : dict[str, list[dict[str, Any]]] } class "QueryResponse" as src.models.api.responses.successful.query.QueryResponse { - available_quotas : Optional[dict[str, int]] - context_status : Optional[ContextStatus] + available_quotas : dict[str, int] + context_status conversation_id : Optional[str] - input_tokens : Optional[int] + input_tokens : int model_config : dict - output_tokens : Optional[int] - rag_chunks : Optional[list[RAGChunk]] - referenced_documents : Optional[list[ReferencedDocument]] - response : Optional[str] - tool_calls : Optional[list[ToolCallSummary]] - tool_results : Optional[list[ToolResultSummary]] - truncated : Optional[bool] + output_tokens : int + rag_chunks : list[RAGChunk] + referenced_documents : list[ReferencedDocument] + response : str + tool_calls : list[ToolCallSummary] + tool_results : list[ToolResultSummary] + truncated : bool } class "QuotaExceededResponse" as src.models.api.responses.error.too_many_requests.QuotaExceededResponse { description : ClassVar[str] @@ -200,27 +200,27 @@ class "QuotaExceededResponse" as src.models.api.responses.error.too_many_request model(model_name: str) -> Self } class "RAGInfoResponse" as src.models.api.responses.successful.catalog.RAGInfoResponse { - created_at : Optional[int] + created_at : int expires_at : Optional[int] - id : Optional[str] + id : str last_active_at : Optional[int] model_config : dict name : Optional[str] - object : Optional[str] - status : Optional[str] - usage_bytes : Optional[int] + object : str + status : str + usage_bytes : int } class "RAGListResponse" as src.models.api.responses.successful.catalog.RAGListResponse { model_config : dict - rags : Optional[list[str]] + rags : list[str] } class "ReadinessResponse" as src.models.api.responses.successful.probes.ReadinessResponse { impacts : Optional[list[str]] model_config : dict - overall_status : Optional[HealthStatus] - providers : Optional[list[ProviderHealthStatus]] - ready : Optional[bool] - reason : Optional[str] + overall_status + providers : list[ProviderHealthStatus] + ready : bool + reason : str } class "ResponsesResponse" as src.models.api.responses.successful.responses_openai.ResponsesResponse { available_quotas : dict[str, int] @@ -260,36 +260,36 @@ class "RlsapiV1InferData" as src.models.api.responses.successful.rlsapi.RlsapiV1 rag_chunks : Optional[list[RAGChunk]] referenced_documents : Optional[list[ReferencedDocument]] request_id : Optional[str] - text : Optional[str] + text : str tool_calls : Optional[list[ToolCallSummary]] tool_results : Optional[list[ToolResultSummary]] } class "RlsapiV1InferResponse" as src.models.api.responses.successful.rlsapi.RlsapiV1InferResponse { - data : Optional[RlsapiV1InferData] + data model_config : dict } class "SavedPromptDeleteResponse" as src.models.api.responses.successful.saved_prompts.SavedPromptDeleteResponse { model_config : dict - prompt_id : Optional[str] + prompt_id : str resource_name : ClassVar[str] } class "SavedPromptResponse" as src.models.api.responses.successful.saved_prompts.SavedPromptResponse { - content : Optional[str] - created_at : Optional[datetime] - id : Optional[str] + content : str + created_at : datetime + id : str model_config : dict - name : Optional[str] - updated_at : Optional[datetime] + name : str + updated_at : datetime } class "SavedPromptsConfigResponse" as src.models.api.responses.successful.saved_prompts.SavedPromptsConfigResponse { - max_content_length : Optional[int] - max_display_name_length : Optional[int] - max_prompts_per_user : Optional[int] + max_content_length : int + max_display_name_length : int + max_prompts_per_user : int model_config : dict } class "SavedPromptsListResponse" as src.models.api.responses.successful.saved_prompts.SavedPromptsListResponse { model_config : dict - prompts : Optional[list[SavedPromptResponse]] + prompts : list[SavedPromptResponse] } class "ServiceUnavailableResponse" as src.models.api.responses.error.service_unavailable.ServiceUnavailableResponse { description : ClassVar[str] @@ -297,22 +297,22 @@ class "ServiceUnavailableResponse" as src.models.api.responses.error.service_una } class "ShieldsResponse" as src.models.api.responses.successful.catalog.ShieldsResponse { model_config : dict - shields : Optional[list[CatalogShield]] + shields : list[CatalogShield] } class "SkillsResponse" as src.models.api.responses.successful.catalog.SkillsResponse { model_config : dict - skills : Optional[list[SkillMetadata]] + skills : list[SkillMetadata] } class "StatusResponse" as src.models.api.responses.successful.probes.StatusResponse { - functionality : Optional[str] + functionality : str model_config : dict - status : Optional[dict[str, Any]] + status : dict[str, Any] } class "StreamingInterruptResponse" as src.models.api.responses.successful.query.StreamingInterruptResponse { - interrupted : Optional[bool] - message : Optional[str] + interrupted : bool + message : str model_config : dict - request_id : Optional[str] + request_id : str } class "StreamingQueryResponse" as src.models.api.responses.successful.query.StreamingQueryResponse { model_config : dict @@ -326,7 +326,7 @@ class "TooManyConcurrentRequestsResponse" as src.models.api.responses.error.too_ } class "ToolsResponse" as src.models.api.responses.successful.catalog.ToolsResponse { model_config : dict - tools : Optional[list[CatalogTool]] + tools : list[CatalogTool] } class "UnauthorizedResponse" as src.models.api.responses.error.unauthorized.UnauthorizedResponse { description : ClassVar[str] @@ -339,41 +339,43 @@ class "UnprocessableEntityResponse" as src.models.api.responses.error.unprocessa class "VectorStoreDeleteResponse" as src.models.api.responses.successful.vector_stores.VectorStoreDeleteResponse { model_config : dict resource_name : ClassVar[str] - vector_store_id : Optional[str] + vector_store_id : str } class "VectorStoreFileDeleteResponse" as src.models.api.responses.successful.vector_stores.VectorStoreFileDeleteResponse { - file_id : Optional[str] + file_id : str model_config : dict resource_name : ClassVar[str] } class "VectorStoreFileResponse" as src.models.api.responses.successful.vector_stores.VectorStoreFileResponse { attributes : Optional[Mapping[str, Any]] - id : Optional[str] + id : str last_error : Optional[str] model_config : dict - object : Optional[str] - status : Optional[str] - vector_store_id : Optional[str] + object : str + status : str + vector_store_id : str } class "VectorStoreFilesListResponse" as src.models.api.responses.successful.vector_stores.VectorStoreFilesListResponse { - data : Optional[list[VectorStoreFileResponse]] + data : list[VectorStoreFileResponse] model_config : dict - object : Optional[str] + object : str } class "VectorStoreResponse" as src.models.api.responses.successful.vector_stores.VectorStoreResponse { - created_at : Optional[int] + created_at : int expires_at : Optional[int] - id : Optional[str] + id : str last_active_at : Optional[int] metadata : Optional[dict[str, Any]] model_config : dict - name : Optional[str] - status : Optional[str] - usage_bytes : Optional[int] + name : str + status : str + usage_bytes : int } class "VectorStoresListResponse" as src.models.api.responses.successful.vector_stores.VectorStoresListResponse { - data : Optional[list[VectorStoreResponse]] + data : list[VectorStoreResponse] model_config : dict - object : Optional[str] + object : str } +src.models.api.responses.error.bases.DetailModel --* src.models.api.responses.error.bases.AbstractErrorResponse : detail +src.models.api.responses.successful.rlsapi.RlsapiV1InferData --* src.models.api.responses.successful.rlsapi.RlsapiV1InferResponse : data @enduml diff --git a/docs/models/responses.svg b/docs/models/responses.svg index d3f41457c..83c8ae562 100644 --- a/docs/models/responses.svg +++ b/docs/models/responses.svg @@ -1,810 +1,822 @@ - + - - - - AbstractDeleteResponse - - deleted : Optional[bool] - resource_name : ClassVar[str] - - openapi_response() -> dict[str, Any] - response() -> str + + + + AbstractDeleteResponse + + deleted : bool + resource_name : ClassVar[str] + + openapi_response() -> dict[str, Any] + response() -> str - - - - AbstractErrorResponse - - detail : Optional[DetailModel] - status_code : Optional[int] - - get_description() -> str - openapi_response(examples: Optional[list[str]]) -> dict[str, Any] + + + + AbstractErrorResponse + + detail + status_code : int + + get_description() -> str + openapi_response(examples: Optional[list[str]]) -> dict[str, Any] - - - - AbstractSuccessfulResponse - - - openapi_response() -> dict[str, Any] + + + + AbstractSuccessfulResponse + + + openapi_response() -> dict[str, Any] - - - - AuthorizedResponse - - model_config : dict - skip_userid_check : Optional[bool] - user_id : Optional[str] - username : Optional[str] - + + + + AuthorizedResponse + + model_config : dict + skip_userid_check : bool + user_id : str + username : str + - - - - BadRequestResponse - - description : ClassVar[str] - model_config : dict - + + + + BadRequestResponse + + description : ClassVar[str] + model_config : dict + - - - - ConfigurationResponse - - configuration : Configuration - model_config : ConfigDict - + + + + ConfigurationResponse + + configuration + model_config : ConfigDict + - - - - ConflictResponse - - description : ClassVar[str] - model_config : dict - - file_search() -> Self - mcp_tool(server_label: str) -> Self + + + + ConflictResponse + + description : ClassVar[str] + model_config : dict + + file_search() -> Self + mcp_tool(server_label: str) -> Self - - - - ConversationDeleteResponse - - conversation_id : Optional[str] - model_config : dict - resource_name : ClassVar[str] - - success() -> bool + + + + ConversationDeleteResponse + + conversation_id : str + model_config : dict + resource_name : ClassVar[str] + + success() -> bool - - - - ConversationResponse - - chat_history : Optional[list[ConversationTurn]] - conversation_id : Optional[str] - model_config : dict - + + + + ConversationResponse + + chat_history : list[ConversationTurn] + conversation_id : str + model_config : dict + - - - - ConversationUpdateResponse - - conversation_id : Optional[str] - message : Optional[str] - model_config : dict - success : Optional[bool] - + + + + ConversationUpdateResponse + + conversation_id : str + message : str + model_config : dict + success : bool + - - - - ConversationsListResponse - - conversations : list[ConversationDetails] - model_config : dict - + + + + ConversationsListResponse + + conversations : list[ConversationDetails] + model_config : dict + - - - - ConversationsListResponseV2 - - conversations : list[ConversationData] - model_config : dict - + + + + ConversationsListResponseV2 + + conversations : list[ConversationData] + model_config : dict + - - - - DetailModel - - cause : Optional[str] - response : Optional[str] - + + + + DetailModel + + cause : str + response : str + - - - - FeedbackResponse - - model_config : dict - response : Optional[str] - + + + + FeedbackResponse + + model_config : dict + response : str + - - - - FeedbackStatusUpdateResponse - - model_config : dict - status : dict[str, Any] - + + + + FeedbackStatusUpdateResponse + + model_config : dict + status : dict[str, Any] + - - - - FileResponse - - bytes : Optional[int] - created_at : Optional[int] - filename : Optional[str] - id : Optional[str] - model_config : dict - object : Optional[str] - purpose : Optional[str] - + + + + FileResponse + + bytes : int + created_at : int + filename : str + id : str + model_config : dict + object : str + purpose : str + - - - - FileTooLargeResponse - - description : ClassVar[str] - model_config : dict - - exceeds_local_limit() -> Self - from_backend_rejection() -> Self + + + + FileTooLargeResponse + + description : ClassVar[str] + model_config : dict + + exceeds_local_limit() -> Self + from_backend_rejection() -> Self - - - - ForbiddenResponse - - description : ClassVar[str] - model_config : dict - - conversation(action: str, resource_id: str, user_id: str) -> Self - endpoint(user_id: str) -> Self - feedback_disabled() -> Self - mcp_server_static_config(server_name: str) -> Self - model_override() -> Self - saved_prompt(action: str, resource_id: str, user_id: str) -> Self + + + + ForbiddenResponse + + description : ClassVar[str] + model_config : dict + + conversation(action: str, resource_id: str, user_id: str) -> Self + endpoint(user_id: str) -> Self + feedback_disabled() -> Self + mcp_server_static_config(server_name: str) -> Self + model_override() -> Self + saved_prompt(action: str, resource_id: str, user_id: str) -> Self - - - - InfoResponse - - model_config : dict - name : Optional[str] - ogx_version : Optional[str] - service_version : Optional[str] - + + + + InfoResponse + + model_config : dict + name : str + ogx_version : str + service_version : str + - - - - InternalServerErrorResponse - - description : ClassVar[str] - model_config : dict - - cache_unavailable() -> Self - configuration_not_loaded() -> Self - database_error() -> Self - feedback_path_invalid(path: str) -> Self - generic() -> Self - mcp_server_registration_failed() -> Self - query_failed(cause: str) -> Self + + + + InternalServerErrorResponse + + description : ClassVar[str] + model_config : dict + + cache_unavailable() -> Self + configuration_not_loaded() -> Self + database_error() -> Self + feedback_path_invalid(path: str) -> Self + generic() -> Self + mcp_server_registration_failed() -> Self + query_failed(cause: str) -> Self - - - - LivenessResponse - - alive : Optional[bool] - model_config : dict - + + + + LivenessResponse + + alive : bool + model_config : dict + - - - - MCPClientAuthOptionsResponse - - model_config : dict - servers : Optional[list[MCPServerAuthInfo]] - + + + + MCPClientAuthOptionsResponse + + model_config : dict + servers : list[MCPServerAuthInfo] + - - - - MCPServerDeleteResponse - - model_config : dict - name : Optional[str] - resource_name : ClassVar[str] - + + + + MCPServerDeleteResponse + + model_config : dict + name : str + resource_name : ClassVar[str] + - - - - MCPServerListResponse - - model_config : dict - servers : Optional[list[MCPServerInfo]] - + + + + MCPServerListResponse + + model_config : dict + servers : list[MCPServerInfo] + - - - - MCPServerRegistrationResponse - - message : Optional[str] - model_config : dict - name : Optional[str] - provider_id : Optional[str] - url : Optional[str] - + + + + MCPServerRegistrationResponse + + message : str + model_config : dict + name : str + provider_id : str + url : str + - - - - ModelsResponse - - model_config : dict - models : Optional[list[CatalogModel]] - + + + + ModelsResponse + + model_config : dict + models : list[CatalogModel] + - - - - NotFoundResponse - - description : ClassVar[str] - model_config : dict - + + + + NotFoundResponse + + description : ClassVar[str] + model_config : dict + - - - - PromptDeleteResponse - - model_config : dict - prompt_id : Optional[str] - resource_name : ClassVar[str] - + + + + PromptDeleteResponse + + model_config : dict + prompt_id : str + resource_name : ClassVar[str] + - - - - PromptResourceResponse - - is_default : Optional[bool] - model_config : dict - prompt : Optional[str] - prompt_id : Optional[str] - variables : Optional[list[str]] - version : Optional[int] - + + + + PromptResourceResponse + + is_default : Optional[bool] + model_config : dict + prompt : Optional[str] + prompt_id : str + variables : Optional[list[str]] + version : int + - - - - PromptTooLongResponse - - description : ClassVar[str] - model_config : dict - + + + + PromptTooLongResponse + + description : ClassVar[str] + model_config : dict + - - - - PromptsListResponse - - data : Optional[list[PromptResourceResponse]] - model_config : dict - + + + + PromptsListResponse + + data : list[PromptResourceResponse] + model_config : dict + - - - - ProviderResponse - - api : Optional[str] - config : Optional[dict[str, Any]] - health : Optional[dict[str, Any]] - model_config : dict - provider_id : Optional[str] - provider_type : Optional[str] - + + + + ProviderResponse + + api : str + config : dict[str, Any] + health : dict[str, Any] + model_config : dict + provider_id : str + provider_type : str + - - - - ProvidersListResponse - - model_config : dict - providers : Optional[dict[str, list[dict[str, Any]]]] - + + + + ProvidersListResponse + + model_config : dict + providers : dict[str, list[dict[str, Any]]] + - - - - QueryResponse - - available_quotas : Optional[dict[str, int]] - context_status : Optional[ContextStatus] - conversation_id : Optional[str] - input_tokens : Optional[int] - model_config : dict - output_tokens : Optional[int] - rag_chunks : Optional[list[RAGChunk]] - referenced_documents : Optional[list[ReferencedDocument]] - response : Optional[str] - tool_calls : Optional[list[ToolCallSummary]] - tool_results : Optional[list[ToolResultSummary]] - truncated : Optional[bool] - + + + + QueryResponse + + available_quotas : dict[str, int] + context_status + conversation_id : Optional[str] + input_tokens : int + model_config : dict + output_tokens : int + rag_chunks : list[RAGChunk] + referenced_documents : list[ReferencedDocument] + response : str + tool_calls : list[ToolCallSummary] + tool_results : list[ToolResultSummary] + truncated : bool + - - - - QuotaExceededResponse - - description : ClassVar[str] - model_config : dict - - from_exception(exc: QuotaExceedError) -> Self - model(model_name: str) -> Self + + + + QuotaExceededResponse + + description : ClassVar[str] + model_config : dict + + from_exception(exc: QuotaExceedError) -> Self + model(model_name: str) -> Self - - - - RAGInfoResponse - - created_at : Optional[int] - expires_at : Optional[int] - id : Optional[str] - last_active_at : Optional[int] - model_config : dict - name : Optional[str] - object : Optional[str] - status : Optional[str] - usage_bytes : Optional[int] - + + + + RAGInfoResponse + + created_at : int + expires_at : Optional[int] + id : str + last_active_at : Optional[int] + model_config : dict + name : Optional[str] + object : str + status : str + usage_bytes : int + - - - - RAGListResponse - - model_config : dict - rags : Optional[list[str]] - + + + + RAGListResponse + + model_config : dict + rags : list[str] + - - - - ReadinessResponse - - impacts : Optional[list[str]] - model_config : dict - overall_status : Optional[HealthStatus] - providers : Optional[list[ProviderHealthStatus]] - ready : Optional[bool] - reason : Optional[str] - + + + + ReadinessResponse + + impacts : Optional[list[str]] + model_config : dict + overall_status + providers : list[ProviderHealthStatus] + ready : bool + reason : str + - - - - ResponsesResponse - - available_quotas : dict[str, int] - completed_at : Optional[int] - conversation : Optional[str] - created_at : int - error : Optional[Error] - id : str - instructions : Optional[str] - max_output_tokens : Optional[int] - max_tool_calls : Optional[int] - metadata : Optional[dict[str, str]] - model : str - model_config : dict - object : Literal['response'] - output : list[Output] - output_text : str - parallel_tool_calls : bool - previous_response_id : Optional[str] - prompt : Optional[Prompt] - reasoning : Optional[Reasoning] - safety_identifier : Optional[str] - status : str - store : Optional[bool] - temperature : Optional[float] - text : Optional[Text] - tool_choice : Optional[ToolChoice] - tools : Optional[list[OutputTool]] - top_p : Optional[float] - truncation : Optional[str] - usage : Optional[Usage] - - openapi_response() -> dict[str, Any] + + + + ResponsesResponse + + available_quotas : dict[str, int] + completed_at : Optional[int] + conversation : Optional[str] + created_at : int + error : Optional[Error] + id : str + instructions : Optional[str] + max_output_tokens : Optional[int] + max_tool_calls : Optional[int] + metadata : Optional[dict[str, str]] + model : str + model_config : dict + object : Literal['response'] + output : list[Output] + output_text : str + parallel_tool_calls : bool + previous_response_id : Optional[str] + prompt : Optional[Prompt] + reasoning : Optional[Reasoning] + safety_identifier : Optional[str] + status : str + store : Optional[bool] + temperature : Optional[float] + text : Optional[Text] + tool_choice : Optional[ToolChoice] + tools : Optional[list[OutputTool]] + top_p : Optional[float] + truncation : Optional[str] + usage : Optional[Usage] + + openapi_response() -> dict[str, Any] - - - - RlsapiV1InferData - - input_tokens : Optional[int] - output_tokens : Optional[int] - rag_chunks : Optional[list[RAGChunk]] - referenced_documents : Optional[list[ReferencedDocument]] - request_id : Optional[str] - text : Optional[str] - tool_calls : Optional[list[ToolCallSummary]] - tool_results : Optional[list[ToolResultSummary]] - + + + + RlsapiV1InferData + + input_tokens : Optional[int] + output_tokens : Optional[int] + rag_chunks : Optional[list[RAGChunk]] + referenced_documents : Optional[list[ReferencedDocument]] + request_id : Optional[str] + text : str + tool_calls : Optional[list[ToolCallSummary]] + tool_results : Optional[list[ToolResultSummary]] + - - - - RlsapiV1InferResponse - - data : Optional[RlsapiV1InferData] - model_config : dict - + + + + RlsapiV1InferResponse + + data + model_config : dict + - - - - SavedPromptDeleteResponse - - model_config : dict - prompt_id : Optional[str] - resource_name : ClassVar[str] - + + + + SavedPromptDeleteResponse + + model_config : dict + prompt_id : str + resource_name : ClassVar[str] + - - - - SavedPromptResponse - - content : Optional[str] - created_at : Optional[datetime] - id : Optional[str] - model_config : dict - name : Optional[str] - updated_at : Optional[datetime] - + + + + SavedPromptResponse + + content : str + created_at : datetime + id : str + model_config : dict + name : str + updated_at : datetime + - - - - SavedPromptsConfigResponse - - max_content_length : Optional[int] - max_display_name_length : Optional[int] - max_prompts_per_user : Optional[int] - model_config : dict - + + + + SavedPromptsConfigResponse + + max_content_length : int + max_display_name_length : int + max_prompts_per_user : int + model_config : dict + - - - - SavedPromptsListResponse - - model_config : dict - prompts : Optional[list[SavedPromptResponse]] - + + + + SavedPromptsListResponse + + model_config : dict + prompts : list[SavedPromptResponse] + - - - - ServiceUnavailableResponse - - description : ClassVar[str] - model_config : dict - + + + + ServiceUnavailableResponse + + description : ClassVar[str] + model_config : dict + - - - - ShieldsResponse - - model_config : dict - shields : Optional[list[CatalogShield]] - + + + + ShieldsResponse + + model_config : dict + shields : list[CatalogShield] + - - - - SkillsResponse - - model_config : dict - skills : Optional[list[SkillMetadata]] - + + + + SkillsResponse + + model_config : dict + skills : list[SkillMetadata] + - - - - StatusResponse - - functionality : Optional[str] - model_config : dict - status : Optional[dict[str, Any]] - + + + + StatusResponse + + functionality : str + model_config : dict + status : dict[str, Any] + - - - - StreamingInterruptResponse - - interrupted : Optional[bool] - message : Optional[str] - model_config : dict - request_id : Optional[str] - + + + + StreamingInterruptResponse + + interrupted : bool + message : str + model_config : dict + request_id : str + - - - - StreamingQueryResponse - - model_config : dict - - openapi_response() -> dict[str, Any] + + + + StreamingQueryResponse + + model_config : dict + + openapi_response() -> dict[str, Any] - - - - TooManyConcurrentRequestsResponse - - description : ClassVar[str] - model_config : dict - - file_upload() -> Self - vector_store_attach() -> Self + + + + TooManyConcurrentRequestsResponse + + description : ClassVar[str] + model_config : dict + + file_upload() -> Self + vector_store_attach() -> Self - - - - ToolsResponse - - model_config : dict - tools : Optional[list[CatalogTool]] - + + + + ToolsResponse + + model_config : dict + tools : list[CatalogTool] + - - - - UnauthorizedResponse - - description : ClassVar[str] - model_config : dict - + + + + UnauthorizedResponse + + description : ClassVar[str] + model_config : dict + - - - - UnprocessableEntityResponse - - description : ClassVar[str] - model_config : dict - + + + + UnprocessableEntityResponse + + description : ClassVar[str] + model_config : dict + - - - - VectorStoreDeleteResponse - - model_config : dict - resource_name : ClassVar[str] - vector_store_id : Optional[str] - + + + + VectorStoreDeleteResponse + + model_config : dict + resource_name : ClassVar[str] + vector_store_id : str + - - - - VectorStoreFileDeleteResponse - - file_id : Optional[str] - model_config : dict - resource_name : ClassVar[str] - + + + + VectorStoreFileDeleteResponse + + file_id : str + model_config : dict + resource_name : ClassVar[str] + - - - - VectorStoreFileResponse - - attributes : Optional[Mapping[str, Any]] - id : Optional[str] - last_error : Optional[str] - model_config : dict - object : Optional[str] - status : Optional[str] - vector_store_id : Optional[str] - + + + + VectorStoreFileResponse + + attributes : Optional[Mapping[str, Any]] + id : str + last_error : Optional[str] + model_config : dict + object : str + status : str + vector_store_id : str + - - - - VectorStoreFilesListResponse - - data : Optional[list[VectorStoreFileResponse]] - model_config : dict - object : Optional[str] - + + + + VectorStoreFilesListResponse + + data : list[VectorStoreFileResponse] + model_config : dict + object : str + - - - - VectorStoreResponse - - created_at : Optional[int] - expires_at : Optional[int] - id : Optional[str] - last_active_at : Optional[int] - metadata : Optional[dict[str, Any]] - model_config : dict - name : Optional[str] - status : Optional[str] - usage_bytes : Optional[int] - + + + + VectorStoreResponse + + created_at : int + expires_at : Optional[int] + id : str + last_active_at : Optional[int] + metadata : Optional[dict[str, Any]] + model_config : dict + name : str + status : str + usage_bytes : int + - - - - VectorStoresListResponse - - data : Optional[list[VectorStoreResponse]] - model_config : dict - object : Optional[str] - - - + + + + VectorStoresListResponse + + data : list[VectorStoreResponse] + model_config : dict + object : str + + + + + + + detail + + + + + + data + + diff --git a/docs/models/successful_responses.json b/docs/models/successful_responses.json index e6be288bb..ed6aa8e46 100644 --- a/docs/models/successful_responses.json +++ b/docs/models/successful_responses.json @@ -1819,7 +1819,7 @@ "title": "Verify SSL" }, "risks": { - "description": "Risks to be considered while applying this guradrail", + "description": "Risks to be considered while applying this guardrail", "items": { "$ref": "`#/components/schemas/`RiskDefinition" }, @@ -7364,4 +7364,4 @@ } }, "paths": {} -} +} \ No newline at end of file diff --git a/docs/models/successful_responses.md b/docs/models/successful_responses.md index 06c8a4fd5..c6d1888cf 100644 --- a/docs/models/successful_responses.md +++ b/docs/models/successful_responses.md @@ -712,7 +712,7 @@ Configuration for the Granite Guardian moderation guardrail. - True: Verify using system CA bundle (default, recommended) - False: Disable verification (insecure, for dev only) - str: Path to custom CA bundle file (for internal PKI) | -| risks | array | Risks to be considered while applying this guradrail | +| risks | array | Risks to be considered while applying this guardrail | ## GraniteGuardianShieldConfiguration From bfc8cd756d170bf2054993fba77dcfe46f701323 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Tue, 15 Sep 2026 09:01:48 +0200 Subject: [PATCH 099/120] LCORE-3585: Added type hints --- tests/unit/utils/test_otel_tracing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/utils/test_otel_tracing.py b/tests/unit/utils/test_otel_tracing.py index 3148cd242..af48fb0cb 100644 --- a/tests/unit/utils/test_otel_tracing.py +++ b/tests/unit/utils/test_otel_tracing.py @@ -190,7 +190,7 @@ def test_set_multiple_attributes(self, otel: Generator[Any, Any, Any]) -> None: assert attrs[SpanAttributes.LLM_USAGE_INPUT_TOKENS] == 100 assert attrs[SpanAttributes.LLM_USAGE_OUTPUT_TOKENS] == 50 - def test_set_attributes_with_list(self, otel): + def test_set_attributes_with_list(self, otel: Generator[Any, Any, Any]) -> None: """Test setting attributes with list values.""" tracer, exporter = otel with tracer.start_as_current_span("test_span") as span: From 1e570e549d285b3b7410471eb74748360b6c2ef8 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Tue, 15 Sep 2026 09:04:25 +0200 Subject: [PATCH 100/120] LCORE-3819: Added missing devel doc --- src/pydantic_ai_lightspeed/llamastack/README.md | 2 ++ tests/unit/pydantic_ai_lightspeed/llamastack/README.md | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 src/pydantic_ai_lightspeed/llamastack/README.md create mode 100644 tests/unit/pydantic_ai_lightspeed/llamastack/README.md diff --git a/src/pydantic_ai_lightspeed/llamastack/README.md b/src/pydantic_ai_lightspeed/llamastack/README.md new file mode 100644 index 000000000..473d0e008 --- /dev/null +++ b/src/pydantic_ai_lightspeed/llamastack/README.md @@ -0,0 +1,2 @@ +# List of source files stored in `src/pydantic_ai_lightspeed/llamastack` directory + diff --git a/tests/unit/pydantic_ai_lightspeed/llamastack/README.md b/tests/unit/pydantic_ai_lightspeed/llamastack/README.md new file mode 100644 index 000000000..7944b07e8 --- /dev/null +++ b/tests/unit/pydantic_ai_lightspeed/llamastack/README.md @@ -0,0 +1,2 @@ +# List of source files stored in `tests/unit/pydantic_ai_lightspeed/llamastack` directory + From bbbac77b9e49a4662e01a6fed008990ef17c3d7f Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Tue, 15 Sep 2026 09:08:49 +0200 Subject: [PATCH 101/120] LCORE-3820: Updated dependencis --- uv.lock | 436 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 218 insertions(+), 218 deletions(-) diff --git a/uv.lock b/uv.lock index 7d6445c00..eb54f1900 100644 --- a/uv.lock +++ b/uv.lock @@ -263,65 +263,65 @@ wheels = [ [[package]] name = "ast-serialize" -version = "0.11.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/a2/04a9383e7512c91c54b2f34b3ff86dc7d2610506f588c2bda36e952a68f7/ast_serialize-0.11.1.tar.gz", hash = "sha256:cc5db2983805f6be786488aac8c5998d5b71965488d1b18c44d435a2205a5cb4", size = 953785, upload-time = "2026-09-09T16:05:27.851Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/cb/3577c66278e4ea4ff6968aee04327447335ecfc58c76123f68cf7f76d75e/ast_serialize-0.11.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f7ca1557553fdab313999ad69156de154ff87152cae963698be2e765164bb6c0", size = 897021, upload-time = "2026-09-09T16:03:59.316Z" }, - { url = "https://files.pythonhosted.org/packages/47/db/9b4eed53ab0bb63653f56ef71062fa91693c8c26b0b648c58d98365c990c/ast_serialize-0.11.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8b54e44763a851c336ca137c60a2434511f0d155e0b924ef374bfbc8d8db8927", size = 1235148, upload-time = "2026-09-09T16:04:01.201Z" }, - { url = "https://files.pythonhosted.org/packages/fd/97/1ebe323015c3cf530f08e88df9929bfd9f5cf8165842bd4b37a7c958565c/ast_serialize-0.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:753afabcc4abf295f515ea33185cf997a1990b88a29d3a3336a9acf62ea85950", size = 1216082, upload-time = "2026-09-09T16:04:02.87Z" }, - { url = "https://files.pythonhosted.org/packages/ac/72/0d3a368edc2d1e0e188c0d3f23821f4e20a6a2291b23d9df482a6b11277f/ast_serialize-0.11.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98b8aba6bb682b9e8859c628382b4881c0600b28056d63c3168c227c449f3284", size = 1282848, upload-time = "2026-09-09T16:04:04.316Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fb/2a546a57d7caa708d739c1747bbdf45cc3aa9dac9407a11d9d67e8a0e255/ast_serialize-0.11.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:651a7890558896c3e08089e51ab3b3a43710eab15b72ad861c400bc9a51923ba", size = 1285337, upload-time = "2026-09-09T16:04:05.899Z" }, - { url = "https://files.pythonhosted.org/packages/bf/16/4ff4179584f8b8bec348787bf721cb7ce3d4c707c7878778065249b59b9a/ast_serialize-0.11.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff8ddc1453bfe7934292409c7b8e9f68b0a6cff4e4c12535e4b4a66fd83aca0c", size = 1554906, upload-time = "2026-09-09T16:04:07.42Z" }, - { url = "https://files.pythonhosted.org/packages/df/e8/e54a54676ca8965e1ae3cfe065dc26c75158e0b3a3ce35af283da106fbf9/ast_serialize-0.11.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:daec16b026c5741950a00c708acdf698c4f17babbe4829dd8232324822e9514f", size = 1301472, upload-time = "2026-09-09T16:04:08.782Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ab/618835a2479868a0d4ac807dd04cf2f89d24afe22c4171538d9779c2772e/ast_serialize-0.11.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9961abcb93bf03652ab09c00862a2396abacd57551c769ad34490a2ae508a61d", size = 1301293, upload-time = "2026-09-09T16:04:10.17Z" }, - { url = "https://files.pythonhosted.org/packages/61/cb/8acc29a1b279ccdfb30dcbdf099e1d8d34c4ee77be9be751c48061433129/ast_serialize-0.11.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:8ca9e48edf246f09fd9bdb64fbd9cd8d18cd2a69ffc24d44891466e17595094f", size = 1307807, upload-time = "2026-09-09T16:04:11.989Z" }, - { url = "https://files.pythonhosted.org/packages/da/74/bada875132f452cb5c1179e3156df43cee6df6cb51f773220688be3d4bb4/ast_serialize-0.11.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a57e0e025e9dcee48e466b3cf909178e298d238148664e796fc0f60ded5e52e", size = 1356265, upload-time = "2026-09-09T16:04:13.429Z" }, - { url = "https://files.pythonhosted.org/packages/4f/ae/720a8042fc8c1d170f4cc7ed46295ef078f8198ed3476f58210a1e675e0c/ast_serialize-0.11.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:110386eebccf200446d5f4dcc215f80866a5d3d3d054d3ced6ec1bf386cd3f02", size = 1459957, upload-time = "2026-09-09T16:04:14.878Z" }, - { url = "https://files.pythonhosted.org/packages/7d/2c/ce2ff1ffd4d70376073393c154033728c8535c17211b7710fee7b0f04474/ast_serialize-0.11.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ef3950747f3a588f692cf4d6f2937eedd1a2d5e199f863af29e8ec7da78a4f36", size = 1562369, upload-time = "2026-09-09T16:04:16.563Z" }, - { url = "https://files.pythonhosted.org/packages/fd/3e/d7a5dd5f609bd0bac45617ce9425e4bd1e369b998c8678edfd70c985408c/ast_serialize-0.11.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0bc94b81878a3f999fe4cdb606bf00ef7dff6b4f66d0e089b2cf7bfc00d59beb", size = 1556466, upload-time = "2026-09-09T16:04:18.396Z" }, - { url = "https://files.pythonhosted.org/packages/99/cd/a45179802a8637f2be252163ec4e4948534fa8d88798dd391c9cc54e096e/ast_serialize-0.11.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:341dfc9e6b4e47e5c37e1b7257a77d39f407cf49a371ee90225ab624c2fe8b36", size = 1687190, upload-time = "2026-09-09T16:04:19.965Z" }, - { url = "https://files.pythonhosted.org/packages/01/eb/ef206d764ec0554368501b8b5268f5468819dc0588815acb0575465643df/ast_serialize-0.11.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7b524cb1b08e15db0f116075c39ab51674b6d3c8103ec1a59c8ca464be83e28a", size = 1481188, upload-time = "2026-09-09T16:04:21.368Z" }, - { url = "https://files.pythonhosted.org/packages/8b/37/9a724c523af7c8b97a659bf4dc28a8e60f97d0cf4d19be277db4067ca734/ast_serialize-0.11.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c2358b341aca08bd2e1005713bc480bc06eee9f345b67fcb826829aa7b8d0d3", size = 1500731, upload-time = "2026-09-09T16:04:22.905Z" }, - { url = "https://files.pythonhosted.org/packages/48/10/890d5fcaf568088de7989e8cdb4ca525f485bd6cb25eea49d45f017500af/ast_serialize-0.11.1-cp314-cp314t-win32.whl", hash = "sha256:b9e61143a5904f46daa0cde74ba60d3b7c0e3e000752ee138e2bc68e126a5803", size = 1119009, upload-time = "2026-09-09T16:04:24.469Z" }, - { url = "https://files.pythonhosted.org/packages/9d/61/2bd32bd16b5e2ee07badc8c32a73c545dc5a9f087ed19c07eee1243d5a1b/ast_serialize-0.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:31df8649761d5cb6eb0de2209bd4a167c7477c0516147c70c0b2f5bddc7839bb", size = 1155665, upload-time = "2026-09-09T16:04:26.638Z" }, - { url = "https://files.pythonhosted.org/packages/5e/b6/84bb5e1dc418e660524b873c5bac9be2bf13d38676c96feef4cd4e7d03f3/ast_serialize-0.11.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2f8f33e416a4c7aad12ad757e895c676e953b4027b80b90718dccea83d45111a", size = 1131854, upload-time = "2026-09-09T16:04:28.099Z" }, - { url = "https://files.pythonhosted.org/packages/8c/dd/16de2c0d23a6b298c735d4e4b56d86fa70476eef98e2c66ceaa65503b62f/ast_serialize-0.11.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:a015ede631eeb098a23de8ec0cfe11a1dd02159ab7a26bdc7c6bde2befbbc7b4", size = 1235495, upload-time = "2026-09-09T16:04:29.654Z" }, - { url = "https://files.pythonhosted.org/packages/27/f7/302d2251e6298bbeabc8a1815c9147127031517b305001a02f34fd46b6b5/ast_serialize-0.11.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:57b7d52d1f5c92905cd648bda9efc83cac33097de1e5bb68de8feca9b5f7e87a", size = 1215642, upload-time = "2026-09-09T16:04:31.186Z" }, - { url = "https://files.pythonhosted.org/packages/93/06/93b6527646613502f364cebfb23783fa6701cdbe90761ee26144f1fa20b9/ast_serialize-0.11.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e47b9b028efbc0486263a49f782ad0ae3e879855fe5a2897b59feefb78e1c40c", size = 1283526, upload-time = "2026-09-09T16:04:32.597Z" }, - { url = "https://files.pythonhosted.org/packages/2f/ac/ffb216262c9a582d039d846081b8c3017dc368cbed5a2520deca5cb7f8cb/ast_serialize-0.11.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00adb748034c7b1938ec56925f9f6230ffcd9c56d47355fe32485df29b90b977", size = 1287526, upload-time = "2026-09-09T16:04:34.214Z" }, - { url = "https://files.pythonhosted.org/packages/a2/06/19a4c4837c4d5e73b982938d6da3c9ae10513b61527575b3e1b8396f9298/ast_serialize-0.11.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:82359d00ea808c260c327e43955c0190bb7baef152a01ae0b5c74d05387b9552", size = 1558354, upload-time = "2026-09-09T16:04:35.669Z" }, - { url = "https://files.pythonhosted.org/packages/e5/29/2373199907b2d97feed176115308dfb3dd0446566b047009828f266d2d66/ast_serialize-0.11.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f35bb9ddeed4008c7d3e272dd1999885d712e1ce623b0a65ba8a8ed1d51c35cf", size = 1302731, upload-time = "2026-09-09T16:04:37.423Z" }, - { url = "https://files.pythonhosted.org/packages/33/55/ab6805a1457dbe565c84f8d36e2aa4207ef22408adfd01d225823cd07484/ast_serialize-0.11.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74093ed682ba58456f0d4450da7fb62b826fddd97824d44fc4a81ca6e0bf9e23", size = 1301594, upload-time = "2026-09-09T16:04:38.861Z" }, - { url = "https://files.pythonhosted.org/packages/52/e2/4f54eab2201bb420d39bf61423abcb5d4daffd28e2270e1c5eee2bf3c0f1/ast_serialize-0.11.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:4dd7218870c203eff4533cfc04a39f29077d6789b20f5b746e7648fe5762a548", size = 1309355, upload-time = "2026-09-09T16:04:40.543Z" }, - { url = "https://files.pythonhosted.org/packages/10/07/755dd98664e2374080b72ccb5fea63d9f11b4bec2409a05b2a4ebc97618d/ast_serialize-0.11.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e8d20700766171a8a17f89cf53d7bd9712c509460d28a51f7f98340577b78aa", size = 1356645, upload-time = "2026-09-09T16:04:42.299Z" }, - { url = "https://files.pythonhosted.org/packages/34/a0/fadbde3e108064236e2679728ce6d8247aefc81bac5fd82505b59cf69172/ast_serialize-0.11.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:20b3371c0403099cd55d59f4bcedb6d3994d9db3fa41711c648760a89ef2a575", size = 1459696, upload-time = "2026-09-09T16:04:44.069Z" }, - { url = "https://files.pythonhosted.org/packages/02/fd/b4f95249bd895368ea8290668e27d1f0a5c4ad888bd72a34c7beb2a500d6/ast_serialize-0.11.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:cc1a78b913f8665dda145999b1b4805ad2ef442ecbe3b819512cf7f95e70498a", size = 1562517, upload-time = "2026-09-09T16:04:45.461Z" }, - { url = "https://files.pythonhosted.org/packages/68/98/6afac594380410710d9d863b9ef41e9c6ca89bd901bda1464ee9e99180a8/ast_serialize-0.11.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:38f7141881e783bccc362d201d7fd7437934216b669b9fef39febc2f63fd2d9b", size = 1556951, upload-time = "2026-09-09T16:04:47.12Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6f/5a81b5cbf1731f4722bc90adf4b4ace7dda69ca29837844147063aeafb22/ast_serialize-0.11.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:13903a3f212ab9e5d05a6c3f2337288f4f8f4c18e6ab3817417ae7a3d46dee14", size = 1691039, upload-time = "2026-09-09T16:04:48.691Z" }, - { url = "https://files.pythonhosted.org/packages/99/eb/d9b903206cbd6d5143838367d385fa88ec49eaae2cc12f284327c6e0af05/ast_serialize-0.11.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:e5a9c53e64732118ae8235d0e57eb90e2333e90ff749a1a7dca6f8bfdfa5200e", size = 1483297, upload-time = "2026-09-09T16:04:50.136Z" }, - { url = "https://files.pythonhosted.org/packages/ed/48/ee13b4079ec67e9b333a87e5bb8eee643ab1b49f7173b3fca71158c6cdf6/ast_serialize-0.11.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:aecb606f67f21fd1c0ffba1826470d2ee400e41b23881ad9a1b2157306865b1a", size = 1501686, upload-time = "2026-09-09T16:04:51.734Z" }, - { url = "https://files.pythonhosted.org/packages/15/67/2175b79e1ee6b042e4bf8ed6871e60cafea7e9cf3fc69e87d7a9d520ae77/ast_serialize-0.11.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:d1ef9c478d8c8ca83499704d13e5ac32c840ed7ca7884aaaf716166aca5a2806", size = 1119522, upload-time = "2026-09-09T16:04:53.248Z" }, - { url = "https://files.pythonhosted.org/packages/f6/c3/8c96aa5f121e9bdda0346b2df3919185eb1ec9a2cce2d20e311b4575ca0b/ast_serialize-0.11.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:27eef0739e0110f5db1ff5dfa38b3dadfcb6f0c31975a2b412f9f2bd72139cea", size = 1157260, upload-time = "2026-09-09T16:04:54.944Z" }, - { url = "https://files.pythonhosted.org/packages/10/bc/8aa663209e335eca73c9e1006197222b6b8dafa6665e513cdd975aa65496/ast_serialize-0.11.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:f9454960bbf185d33c669ccd5c9b76c96709a4542a95a65e27342c4d10dd8e20", size = 1132060, upload-time = "2026-09-09T16:04:56.501Z" }, - { url = "https://files.pythonhosted.org/packages/7f/a7/e738887429de70350d9e25f84a95efbdd086b79579a811509dee8b02d7d5/ast_serialize-0.11.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:7b9a6089f1337838492b217707e9a55d0b9b407fe9169a960fa12282abf2234c", size = 1240585, upload-time = "2026-09-09T16:04:59.616Z" }, - { url = "https://files.pythonhosted.org/packages/a1/f9/46b64fa883f3c8b8cf51629978f16d8a2da3d5c5c02f64add40864907703/ast_serialize-0.11.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:68929da7fb1c7375f69641baac9519d5c41ee441cd09a591ea5dfc83107ffe6f", size = 1228038, upload-time = "2026-09-09T16:05:00.948Z" }, - { url = "https://files.pythonhosted.org/packages/ad/89/fe75c8d0f104a4be11cd0e23acecf129484c9ba06c7130033752d63a78c5/ast_serialize-0.11.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8945557ed3015173dbf8d44184da9621add9162720acbc0ac4832b5a091b4e8a", size = 1292388, upload-time = "2026-09-09T16:05:02.498Z" }, - { url = "https://files.pythonhosted.org/packages/cb/56/3f21c881900d8254a35195e1e962355b4d570ccd9cc193b0ba08abb81952/ast_serialize-0.11.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9cbed53bb8992b72e587dd47d0ca71703f5f715420f48ed57587c06c2fcd213", size = 1294413, upload-time = "2026-09-09T16:05:04.212Z" }, - { url = "https://files.pythonhosted.org/packages/f3/b2/8d77be3dad1139158c59e370391f4990ea73f7f102983392272856bd3a63/ast_serialize-0.11.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65c836d1ab7af65e64a1dd73de82572566e838dd27529b1124eac0ac86f35e76", size = 1565921, upload-time = "2026-09-09T16:05:05.753Z" }, - { url = "https://files.pythonhosted.org/packages/94/2e/0675b5796c897f957fbdac6af0ebeeaa9a63cee866c1c54f1e5136ef5bf6/ast_serialize-0.11.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ab6df9898600ff45149b86c12bb5aa3359cc71843ad99ad0c924d11392469ab", size = 1312160, upload-time = "2026-09-09T16:05:07.269Z" }, - { url = "https://files.pythonhosted.org/packages/93/c2/1749fa4efae0dc98aad3c3d29f0178e2609208c3ba34700b4551082e40d5/ast_serialize-0.11.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3eebd3dd25a81839ac7d25b5f3fb3527f6141b35eca29f63d2613fe6254307fd", size = 1312405, upload-time = "2026-09-09T16:05:08.753Z" }, - { url = "https://files.pythonhosted.org/packages/00/14/2afc9f9d7db551f805b4d5e496946dc3dc2a703d27294ccc200b6ff07a7a/ast_serialize-0.11.1-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:311333c5f58f55fc04121be2c5adffa42a19e1038a43a2cef862fdf58c45bf4f", size = 1319458, upload-time = "2026-09-09T16:05:10.552Z" }, - { url = "https://files.pythonhosted.org/packages/c7/4a/8b2866d9d6d5d0b037d7bf2b74db6e75a66695c503248632116f24009a8d/ast_serialize-0.11.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa8d0a9f6d02e7626cdf540555bc2f5313eabe803dcca04854fc628ddf4b1058", size = 1365055, upload-time = "2026-09-09T16:05:12.069Z" }, - { url = "https://files.pythonhosted.org/packages/47/17/73119504574f9b46610ffb718501b254c8adb727d4e43c5babc3c62d4529/ast_serialize-0.11.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e209a797fdf8680d58d2e969a9ddb25058ea682f6cff59dbabc98d8aef4e0db1", size = 1467771, upload-time = "2026-09-09T16:05:13.458Z" }, - { url = "https://files.pythonhosted.org/packages/56/f5/776bfb7a856ba0bb9bd373b76a90230fb77b1a10c987810b621ed244d5ba/ast_serialize-0.11.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:b391becd31e9889da438d388aac0362e3dfd222affbe45d920580c351486c787", size = 1571459, upload-time = "2026-09-09T16:05:15.15Z" }, - { url = "https://files.pythonhosted.org/packages/c7/43/86a655170ee36a8fdce65a922b7e34040bb319eb4ec251161032343c1fc3/ast_serialize-0.11.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:021afb3482d27d1dace9c8999724a13083e96c7ece785e53e418bf5df3b50e0a", size = 1568959, upload-time = "2026-09-09T16:05:16.677Z" }, - { url = "https://files.pythonhosted.org/packages/ed/b1/015a3a156dc9bd46e9c4e4f24a939eb0b87179b1c53c9495ac6f088f13b4/ast_serialize-0.11.1-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:c1dc60251d93beff32d4147ecd4ccc518b0f022a900093f6f3021c2012416f0f", size = 1698173, upload-time = "2026-09-09T16:05:18.248Z" }, - { url = "https://files.pythonhosted.org/packages/0e/ea/a988b70980aca3a8a3fb731901dc911fe73a456cebbc225b767bbe5490ff/ast_serialize-0.11.1-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:b77d6a2267227d68d8221cfca177f8c6d9f68019f3250401d0355d2638db9fb9", size = 1493298, upload-time = "2026-09-09T16:05:19.806Z" }, - { url = "https://files.pythonhosted.org/packages/de/15/e049b2701ddab9087d7f62448f4c2c1b1463e676baa471345cf3e4a31e1b/ast_serialize-0.11.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e68ac7d7fd1a5d42a3a6d726d7cc8a7e0c9987934f11c6a5162aa2cf68e81e24", size = 1510213, upload-time = "2026-09-09T16:05:21.402Z" }, - { url = "https://files.pythonhosted.org/packages/46/a2/23d555eb842d1397dce54fac31851e6f151e03a0ae74db389d9b39718c33/ast_serialize-0.11.1-cp39-abi3-win32.whl", hash = "sha256:ed449d786b9032a7b85fcd6c2f7f54c4cd0e1e1ad254d396805ff922096bf08d", size = 1125239, upload-time = "2026-09-09T16:05:22.796Z" }, - { url = "https://files.pythonhosted.org/packages/1f/4a/251f3fd1b8a5549edaedf8f3ba0b9fb5060e194d3c0ed208b593fccaeff1/ast_serialize-0.11.1-cp39-abi3-win_amd64.whl", hash = "sha256:6b43f5a9b9a8dd20ba3124914e63aa7d7427de761ac849081cda403c2112fda0", size = 1164260, upload-time = "2026-09-09T16:05:24.714Z" }, - { url = "https://files.pythonhosted.org/packages/5e/b5/08e2f643feb9e4d72d42a484949ad51238b18262a01c7036196c201cc330/ast_serialize-0.11.1-cp39-abi3-win_arm64.whl", hash = "sha256:a579ea6f473aab3958a64734194b22fbaec108ec45d994c28d3bd721e1e1da32", size = 1137533, upload-time = "2026-09-09T16:05:26.095Z" }, +version = "0.11.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/1e/4f6082cdd6e5a29093513e9a3eabc5ed1c5331a9a84386b2fece80a00a48/ast_serialize-0.11.2.tar.gz", hash = "sha256:976a5bd75845d22f4b52905ddf53ab669ef1b14dba7735f5512841a2ef2b5450", size = 954387, upload-time = "2026-09-13T18:48:55.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/2e/beec3364eef4b01793a676d8cd16e9014c42044a5505000ceae3955e33fa/ast_serialize-0.11.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f6a8dfc5ab204a706f6e5d39c6f77c18c27ef084fa2081803a64a9160ce89277", size = 897089, upload-time = "2026-09-13T18:47:22.69Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d7/ef56443df2891c6ba2c4019c2cb3dcaf97c9948da6d963068e04e8dac6ea/ast_serialize-0.11.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:cb073bfa15742699d408ac50f60878383b5665ae1791d1b6799ea6f08633cd77", size = 1235218, upload-time = "2026-09-13T18:47:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/42/8d/cff58d17ba1d0272ff0b7ab5d3bdfcf8f47317eb0f47c001d394bffebf95/ast_serialize-0.11.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1d6ad94edbe93bf1dabc06c9f37d55b898fdabc456aa6d7ced5e23c14f795f32", size = 1216399, upload-time = "2026-09-13T18:47:26.202Z" }, + { url = "https://files.pythonhosted.org/packages/de/d2/a1da7675af5f42335c36e4da6d86ef4fd7168cead18de81df0a2d6faeb1a/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40b2801cf2221bd922d9f69d2f0ebc373c3db47207315d525b2d87fa161a2af4", size = 1282064, upload-time = "2026-09-13T18:47:27.787Z" }, + { url = "https://files.pythonhosted.org/packages/97/89/5a400a13b2c9c0152ebb5ad45408a3fe5e4e60e325d3ac4e5cf6e915a0cc/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd666cebd6ab3b3c0fd348a6202c26e18a401ee34293c3804d3472266bc146f6", size = 1285864, upload-time = "2026-09-13T18:47:29.667Z" }, + { url = "https://files.pythonhosted.org/packages/02/b8/80a381c70fd49f0316fb0383c4f9e4c13e81b010b64889bd45898ce8f5f4/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d01f61352c96370febf6c0dbd488dee9183a731fb2702170da9163ae317cded", size = 1554755, upload-time = "2026-09-13T18:47:31.257Z" }, + { url = "https://files.pythonhosted.org/packages/90/97/dcaa34a32d2db789221c125b3eb10feb5089715fe53d9874d627afc26231/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0fd40c668b0fa19b8fdb61d9e63d547e2e19cfbfe053a51ef0b6c37070298a8", size = 1301807, upload-time = "2026-09-13T18:47:32.714Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/84a22420cb312642d7d31547c644d09a3d101418c6d6b9ef2ec30735cf11/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efa819d7c14c8e4153dcd84671826331538be7cbe460383fc6386f5eea5bd234", size = 1301941, upload-time = "2026-09-13T18:47:34.418Z" }, + { url = "https://files.pythonhosted.org/packages/20/8a/aa5f3dcf1aed9678c25982f40d366004e3c0cac47bc0c240f6b837dcbb1f/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a9ffa8a197a721f07a352d0be6185f5b3e6f9aaebfdb66169ed652108531ae3b", size = 1307910, upload-time = "2026-09-13T18:47:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/78/79/91a5102797fe3dc992171382d8579bcb33cbd1424b864ad3117ac43fb3fe/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00119a8fb8c1dc0f1fab023f4d8071fa49e3b0208ee54d589fd463c16ab0124e", size = 1356258, upload-time = "2026-09-13T18:47:37.984Z" }, + { url = "https://files.pythonhosted.org/packages/51/52/54eeef9918e187ced417c4363eecea66975314cd5b9c91759eef7f7b714b/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0de02520c11391a026e62987a9aa2c3c2ff01545155059ddf0c4bdf2c5ecbe9f", size = 1459057, upload-time = "2026-09-13T18:47:39.891Z" }, + { url = "https://files.pythonhosted.org/packages/e4/cb/fd84b52b15d42f2423319cffd1fb7f1e9df5d5198e69ab0b449c450254cf/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b4e4558956b6a0fb35e18fba58f7d1810b1f2c0e6b52352572cd5dfb6b4ef33a", size = 1562447, upload-time = "2026-09-13T18:47:41.727Z" }, + { url = "https://files.pythonhosted.org/packages/be/92/9fb34f2e64b84a63cca92fb86bd0847b995a63b67477f44c20502fb60352/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6061a54f39e82a9f2cbcb9c268fc441890e4818a6636473caa4f4063254e0750", size = 1556423, upload-time = "2026-09-13T18:47:43.357Z" }, + { url = "https://files.pythonhosted.org/packages/75/0f/c43c44449e7ebc4e83ebd48750088fb06234622faa2d62d2a6dc8970d2a3/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:85fbb01e83967a126d71f679f2b9528ef0912cb0854aa1a4657314c34e255b57", size = 1687156, upload-time = "2026-09-13T18:47:44.995Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a7/9e520f4a79b639da9ee20c1e747c3d739329e902fc55ac38065f25419f56/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7aaaffc32905159774a107d3cf33dad59bd41b7a0d1bc9885532186753ee7439", size = 1481008, upload-time = "2026-09-13T18:47:46.602Z" }, + { url = "https://files.pythonhosted.org/packages/48/a8/bdd3989f19de09cffcd8179c131f6741a5a8619705fc75b09541ff61530b/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:08eda88a0f290a36c38cab33df8bf7e35eb95bc802ca5beb2c8fcda471a7d10c", size = 1501597, upload-time = "2026-09-13T18:47:48.265Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8a/ca2dce2950875a4ef1d7c298196f803b0adcdb7c15ed0cecc71d84bccd70/ast_serialize-0.11.2-cp314-cp314t-win32.whl", hash = "sha256:76cc294246e60a914326b4ca88c6a5ea89c064906614aaf1537ce82f09e9449f", size = 1119503, upload-time = "2026-09-13T18:47:49.896Z" }, + { url = "https://files.pythonhosted.org/packages/5a/12/3f38e3613d07c46f9f81c5b1352748c6552397cc52825502e2c6ae44c6ea/ast_serialize-0.11.2-cp314-cp314t-win_amd64.whl", hash = "sha256:43b51e6ebe6549bf21416c3c78ee886147b80875a87cc6f69e303dde0d75be0b", size = 1156828, upload-time = "2026-09-13T18:47:51.454Z" }, + { url = "https://files.pythonhosted.org/packages/22/90/f89a4f67428a261daafdb69a0d0132c27933268702d1ba47e0b61c51aff1/ast_serialize-0.11.2-cp314-cp314t-win_arm64.whl", hash = "sha256:8df32ad4ff7843734a6c2f067ee974f6d3109ee5a2c3e1a9d2f79347bd282a9a", size = 1128298, upload-time = "2026-09-13T18:47:53.008Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/a1962188abf0e62d84d55892bb044347e434711763b9a1d4ad867a70c1be/ast_serialize-0.11.2-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:ab924ba260efd7509492f272d4e236d24564033f20c005d7c63a107c6a76fc85", size = 1235457, upload-time = "2026-09-13T18:47:54.554Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ad/439c2959150718446af76fbe2f4000f35eba9869ef8564f3d9a3d0b1c370/ast_serialize-0.11.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:a586be418eb70a9f1396cea29ddac8f4b9bf277fb73ea2340db31e218bc00f32", size = 1215705, upload-time = "2026-09-13T18:47:56.178Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d8/2c6542fc3e7c56a0a25d8d12d034d5a2d2e1900e292567b1c1dca8e83124/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8532f20916fa3189d4d785ef2a62d93c4d651ec9c5bffda66d2fc36898351f34", size = 1282530, upload-time = "2026-09-13T18:47:57.619Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ad/6f6755cd0842db46c3b10b1e4735f14aad78d71dea4753eb46933101711b/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee732ae167e686d1d3c00f98d7d82b23138304694f0441b14d7ddf9c0f8a921c", size = 1287792, upload-time = "2026-09-13T18:47:59.227Z" }, + { url = "https://files.pythonhosted.org/packages/03/40/5da672f5dd23fb7dc0c884c97711e56a3540f2fe3c4355a81f8beb385911/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75a1c7f46b9c19fc0ae01ca6fd076301628faa2ed7a8edbd55c6353c483946a3", size = 1557971, upload-time = "2026-09-13T18:48:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/df/c7/2bb25684f697801eb72866fdb94ed5edbff3867ce878b0e542a4a5b9dab9/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdf31a0bb85ea2575cc91669f005e6647d2efed491231c4dc1497bc9a5b3aa6", size = 1303230, upload-time = "2026-09-13T18:48:02.337Z" }, + { url = "https://files.pythonhosted.org/packages/d8/85/754681846f26e0ff1da729b1ffe3171e93c22f0aa6ec3cea5b14e3703846/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b78e6fdef3b06c86ed263e1962fee5a7b9d2d158e738b212d13b2c605ee12f5", size = 1302271, upload-time = "2026-09-13T18:48:03.915Z" }, + { url = "https://files.pythonhosted.org/packages/fb/dc/f5521d8cb44b69095c3982ae3658a12c403e0efa19e51aeb9c8a79dff60c/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:8d62a47714c8bc432b9fabcc29989c815c5da17327d35151f2fd0d85c2a7a5ff", size = 1309529, upload-time = "2026-09-13T18:48:05.562Z" }, + { url = "https://files.pythonhosted.org/packages/73/0d/649182c7fd7c4f782279bed514de2dd67e48a5afecb605a098d64fdc01fd/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a5ffa70e76191dcf240d3c43e20c93b3bfd26f54d89148c762d57837f5bcd2c", size = 1356869, upload-time = "2026-09-13T18:48:07.534Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cc/aff4d84c16afa742d13a75384127c7d24594dc8c304f0558a15924fd51af/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:bfbe47a3a7c368f28836e78b2440a3643ac0ec4c67d9fe53588e1448f0a3d35d", size = 1460006, upload-time = "2026-09-13T18:48:09.162Z" }, + { url = "https://files.pythonhosted.org/packages/94/a7/891cbec2e5e0d7159196159d3ff0646622f3120ff4576c839ac2dd56c719/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:7f1823275b246f9c7d373be6879e4eec09686948895d4ad083f4b27fd7e4da70", size = 1562935, upload-time = "2026-09-13T18:48:10.978Z" }, + { url = "https://files.pythonhosted.org/packages/45/c4/2c8c4498340ea9aff87a9fd408309aa25d56dd51d7bbddfdb46a3c31424a/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:57c0f5cb0021a5beb1e5e4d6e840ae2f23a28909703ef4d256a144cc1ad3d437", size = 1557109, upload-time = "2026-09-13T18:48:12.616Z" }, + { url = "https://files.pythonhosted.org/packages/0d/8b/c5d4e5226fa18885fe17f949aee3ab1aeb8389c384d946ec1b7c9489cc94/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:cd320a5c4f1f2742af97eea22954f776379175c5ef2504801e9a155f2ff9a4d7", size = 1691603, upload-time = "2026-09-13T18:48:14.293Z" }, + { url = "https://files.pythonhosted.org/packages/73/d6/1d2ca472586f9e3416a289a22f36eeb6dd6f47d77b1a4aba358405babbc7/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:13b13afe32e845c86a573497729e1b7ddeb26c572c78bf50ece51da23b8fad5e", size = 1483053, upload-time = "2026-09-13T18:48:15.789Z" }, + { url = "https://files.pythonhosted.org/packages/0e/16/d3703a7c1e3c76b144ac9349a54d3926d0749918a8dc13a66cede208b8ec/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:9d80a81ec84660422579bdb8e789f656a794b48c7a1ae1261f6bd8bc1897d17d", size = 1502499, upload-time = "2026-09-13T18:48:17.405Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e4/d974e55c2e247ef26ed1df01c74940583db9a5b3a8bcaad5732c6e2047fb/ast_serialize-0.11.2-cp315-abi3.abi3t-win32.whl", hash = "sha256:af8c003ce721b0099dd55cef4ba733500fc3054ea0cc8565d8957aaf7cccdeb4", size = 1119739, upload-time = "2026-09-13T18:48:19.005Z" }, + { url = "https://files.pythonhosted.org/packages/0d/00/d229443488e095054d5e0c0cc20689a2633b899d735849ff1b2c8e4f0cbf/ast_serialize-0.11.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:554d117cb916d8032d85007c654d179efbbfd446174c048062778136a922944f", size = 1158602, upload-time = "2026-09-13T18:48:20.524Z" }, + { url = "https://files.pythonhosted.org/packages/11/75/389fc1a6cfa0c4b2ce522f47d8401329d8bb11732e516d46465960fef1d9/ast_serialize-0.11.2-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:d60515335750d431e462af6e722bb55720a5e7827192777bddfd9c4376065a4d", size = 1128842, upload-time = "2026-09-13T18:48:22.052Z" }, + { url = "https://files.pythonhosted.org/packages/b1/54/f67120006fc73a55b6d057d4662d061fbb4eceafce3047c76ca8b382eb11/ast_serialize-0.11.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:daadf1c3e0224621607ffe16f1379e4bd372271ed2e1db8a67878f0bab3ef7e4", size = 1240734, upload-time = "2026-09-13T18:48:25.287Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7e/8f2ab68bddbe58a66fbbaad87beeae3e7d7edddb17263d1fc423936cf34d/ast_serialize-0.11.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1844ed9a487fb3de7325c52ddb33f2918b66b65cd54d3f8d83d23785ffe99fa4", size = 1228053, upload-time = "2026-09-13T18:48:26.788Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1b/8a69ab68f4c1603819f0481d756abdd8caf27cec7f1d77caa71007ebe997/ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b17869f4ba261a5fa468a753328a548f4dbaf74b4eadae9e28aff66df7f1425b", size = 1292542, upload-time = "2026-09-13T18:48:28.295Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ce/872f2e00f0467c289e483f0a34543463347243a2d0632748d89fcee5e0dc/ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feb16d9c2a720e0120c58dd5d6e7b3c7c86b43249b60a3bc212bcb8fa031e2dd", size = 1294791, upload-time = "2026-09-13T18:48:29.969Z" }, + { url = "https://files.pythonhosted.org/packages/3a/82/36277c12af861c64b375c316135d8feffe3f400568463a8d2b2de4c2c4fb/ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3109fe4805384effc8d0f8e41fbf875aa8f389af91b4348c1cfb60ea6e4cb82", size = 1567583, upload-time = "2026-09-13T18:48:31.85Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d7/ec643df91cea8bcbcb4e8011d6a8b08e5119b84f9554879f3e3c786d29d1/ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abdb3e49ba053c3486ac1263bee9f16cc9a4a8abd9f8c90bfc21e3669f3ad9d1", size = 1312878, upload-time = "2026-09-13T18:48:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/04/6f/4c992cd7841ba589fefb14ddc9aff2f6db7f2a615d4074f9ad04115b5ce0/ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7004ba572f09be34342ccb98dcd4bad5707d3d81adc8cb4c3f685d2a2c51bbc", size = 1312642, upload-time = "2026-09-13T18:48:35.294Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/22aaa209c231a83cfea004fd67dee7a7a54da3f169c6c460b14b96887385/ast_serialize-0.11.2-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:59c25f47524efa052971b860e128b1add0c94ede7dd16b2962952c85c3582365", size = 1319776, upload-time = "2026-09-13T18:48:36.866Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f7/d4685fb54d10108ce44d3bc893ef670854d61645d47ed96d73524db90c23/ast_serialize-0.11.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3a367e0e05ed2d1b747ceb07aa728a8c204cc008b589127e9bd4f40053d7575", size = 1365324, upload-time = "2026-09-13T18:48:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/42/3a/250643ffad02bda520c50a9a5f02a5d43259a06f34ce393c91761d134d7e/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:00bbf1f6669f813b48925b759f7ae4591067d456d443924055cab386e7e0a719", size = 1467653, upload-time = "2026-09-13T18:48:40.348Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/af66a646b9b7f8fdec95ce83fc7b1fe538b06864bc79bd554ac4fae2e6ea/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ec1c20f89c3e0d83576e3c06f79375ce936266591fe0d5fd969914af3185cbaa", size = 1571914, upload-time = "2026-09-13T18:48:41.968Z" }, + { url = "https://files.pythonhosted.org/packages/34/82/77a9714564b9e8800087a8afec41527c65c39e49282baae2ac847b9c1c6a/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:c58bb119b73657fdc5569692f316e1e25ca114bd62f7782eb527c6be438ba3a9", size = 1569862, upload-time = "2026-09-13T18:48:43.701Z" }, + { url = "https://files.pythonhosted.org/packages/65/06/fa77b52f46b9bd6dcd8ff2b880e3781f8c1a316bb1342bc3de92907c6f96/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:f739e0b601be7300c5697a2573d9200bd1db74b34ab111ef9537b9d5dcd7f106", size = 1699020, upload-time = "2026-09-13T18:48:45.261Z" }, + { url = "https://files.pythonhosted.org/packages/e1/09/239c83153c7e0798e5867d6909cb06f53dccfef02f6999c8e2e21ecb98c3/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:cae5addfbb54cc1d47fe947ef9138e9d83849ed1cbc72b819cf36d96a2315b07", size = 1492869, upload-time = "2026-09-13T18:48:46.922Z" }, + { url = "https://files.pythonhosted.org/packages/2f/eb/6108fb9a43fc7ab5529856e38e33c6e3e064fbfe375fdcbb208c7cd5438d/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2fa3be25f7f5351b1b39c9f8a52779b2dbf21199efbae564b4746422e8edca4e", size = 1511621, upload-time = "2026-09-13T18:48:48.667Z" }, + { url = "https://files.pythonhosted.org/packages/8a/82/60367e58ef346a41ebc90d3f28593c1b8f5c2cb5314c7b2bbd98910ee131/ast_serialize-0.11.2-cp39-abi3-win32.whl", hash = "sha256:d70556a2f9230a44c99a655774cde823f056efc34466eabfb4085f0cb1ea9f99", size = 1125873, upload-time = "2026-09-13T18:48:50.661Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/b419c3205ce1143ba7c69baef4f0ba43c14d8712113bf34f9e0d27d609be/ast_serialize-0.11.2-cp39-abi3-win_amd64.whl", hash = "sha256:b9065dd23131a23b41f5bab3bf4e9b3c350a3fe8e36e8200eded9b729fcea484", size = 1165434, upload-time = "2026-09-13T18:48:52.169Z" }, + { url = "https://files.pythonhosted.org/packages/91/a7/c8bbb2173f7a7131b3b2412035b2d814ab5ef2ce9799bd06f07c451640e4/ast_serialize-0.11.2-cp39-abi3-win_arm64.whl", hash = "sha256:dab599cbdcb7b45b18c41fad746645580b3a24357082b7f0e8921cd373804f27", size = 1136031, upload-time = "2026-09-13T18:48:54.04Z" }, ] [[package]] @@ -831,71 +831,71 @@ wheels = [ [[package]] name = "coverage" -version = "7.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d1/f5/deb1a27aa20746c0278ac998c4179e272004699b2d33959ce020c5ac1615/coverage-7.16.0.tar.gz", hash = "sha256:077f0964087883176ff6ab9b074694cae29f8c708273b13ca62c183c6ed716cd", size = 945620, upload-time = "2026-08-28T21:54:37.74Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/9c/8d2688694f53dc0b0f0e4783c7eb3c4bb1e79beaf1411879f6dabedf4607/coverage-7.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d1c77c3579ac42798f8b7eed6d3dd258debacca32c8753fc8a1f6eaf1db644f5", size = 223194, upload-time = "2026-08-28T21:51:27.767Z" }, - { url = "https://files.pythonhosted.org/packages/ca/11/f002163dd688aa3fa49ac6a424b7c2705c7fcf80fba18ec9f586d77827ca/coverage-7.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f81cb1554c3712e41649ed5dc98656b50b958e4da12f0f5adb681ce3db92831", size = 223553, upload-time = "2026-08-28T21:51:29.46Z" }, - { url = "https://files.pythonhosted.org/packages/81/65/f9d469e97c4554372a710650a109004a2434dfc56f577142e5d6057fa0cc/coverage-7.16.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e701938ec9081d3e400a0c9a9a8ae0f7ca44214741daeac4454b1c6ef6dbd19", size = 255054, upload-time = "2026-08-28T21:51:31.54Z" }, - { url = "https://files.pythonhosted.org/packages/95/29/dd89fd39af1a3b6e9a9c3eddeaf03f6376ba517d43d6cbf8b519177e2a10/coverage-7.16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:719a3feb6220dd32ed932d4c3676d17fb8739e2643b29c0e7c3af400ff80ac44", size = 257790, upload-time = "2026-08-28T21:51:33.374Z" }, - { url = "https://files.pythonhosted.org/packages/0a/64/208d26cedc525d6b5db9c492cf9130784c42d9eb08d22badaa7b806005ad/coverage-7.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87771ecf986cff55e87413238cd5e4f54d949c2074bd6fc1657d26a56314ee24", size = 258904, upload-time = "2026-08-28T21:51:35.096Z" }, - { url = "https://files.pythonhosted.org/packages/1f/98/28e2752aa9a8baee5798edade9c95602ca200f4e7eeb503eb64df42e5921/coverage-7.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47d5e1fc0b321c8308a2aacee0497c435b08acaa629b7059798fdf6fc3006352", size = 261165, upload-time = "2026-08-28T21:51:36.744Z" }, - { url = "https://files.pythonhosted.org/packages/eb/77/fa6ae699a0ea2bc12acb38a85d96b786fea0f833c12b5756056350e0e547/coverage-7.16.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01b18b8a6c9cec8d5f45550e2501426ed982cf2c35016b0acd2ba9b5d8b2fb06", size = 255416, upload-time = "2026-08-28T21:51:38.495Z" }, - { url = "https://files.pythonhosted.org/packages/89/c8/5ee46d1de7d34cb00ba08b5c50da1971114dbc09ca9898ccc32975ec74dd/coverage-7.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:32c56b5b47c50635081445ac404dd08c2d591b9c837c22570aa9e182c3b42cd4", size = 256825, upload-time = "2026-08-28T21:51:40.27Z" }, - { url = "https://files.pythonhosted.org/packages/15/f6/d59e1c0693ad48855fe20169fbf6ee5befefe5887a7fabf5f0bcb464a2dc/coverage-7.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6ad3bbad240ab937512156bc944fdee63ac4dd34a7558a3094548fd4c1150c02", size = 254970, upload-time = "2026-08-28T21:51:43.136Z" }, - { url = "https://files.pythonhosted.org/packages/df/7b/b51bbe05b3a7565927fccfb1be42b8b3c1f4ab15e53d91b303e9923969aa/coverage-7.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4c1f16d5555a195295d0dc9c902612270e3dfed6a11f3bf7bc470b7b6a79ed3c", size = 259039, upload-time = "2026-08-28T21:51:44.983Z" }, - { url = "https://files.pythonhosted.org/packages/fa/04/d513f816456a8a43c1859abe88a37d01d7d2515b6c3e24ebb3c9b1dd44ec/coverage-7.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f6c9c21a8bf0d19788f3c5f3e020c90317a0a63ef60521b376003801e21250fb", size = 254539, upload-time = "2026-08-28T21:51:46.733Z" }, - { url = "https://files.pythonhosted.org/packages/dc/54/5542190ceb97e0d1333a4ce0c8f95b2ef2efe790f1ad018a4b61766f849e/coverage-7.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f20145a9eb5bf1fd1dde3c0bc2af2e7c22135ab07ca6284d6ada7cc3904c4e", size = 256410, upload-time = "2026-08-28T21:51:48.363Z" }, - { url = "https://files.pythonhosted.org/packages/ee/28/78643f361ff6bb5b2ade90f8bfc8395fe9ca367a18c101f8991215b4c65b/coverage-7.16.0-cp312-cp312-win32.whl", hash = "sha256:916cf8d25c1ce148f7eceb1d45afc9724841200110adc4e53250391852debd91", size = 225239, upload-time = "2026-08-28T21:51:50.22Z" }, - { url = "https://files.pythonhosted.org/packages/67/61/8e76b36c36b1a033dc933dd2480db96b04ce3be975793ce3fad122e7174d/coverage-7.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:78f8b56261d608be102c62edd3a60b66bcd0b581f3f86fdcabaf8b8d95adc950", size = 225775, upload-time = "2026-08-28T21:51:51.912Z" }, - { url = "https://files.pythonhosted.org/packages/c8/f3/bb4787a4b81c1792ca69b502f5f730dbbb609f73fed552ab074c6b92cb8b/coverage-7.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:577c2ac8c0036f6f8edd3a7783a9e67302b17771d1abf0fd2ed246e3158be51b", size = 225159, upload-time = "2026-08-28T21:51:53.667Z" }, - { url = "https://files.pythonhosted.org/packages/54/c5/e62c87f4799d1e3647d5b2ae16ea1d12205d72fde1ea8529e13fe050f678/coverage-7.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1545c52ce756b8a97007f439a220297f1cd72a2cbbcdffccdf1c1f70e74f9a42", size = 223215, upload-time = "2026-08-28T21:51:55.628Z" }, - { url = "https://files.pythonhosted.org/packages/89/e9/5e62fda9397175fb206f75368b6e85da06d831c181b6d0f67ca073cd2f89/coverage-7.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0598aadae641f30a0796b75b45c0b9c5de8619bd5cfb251bb0cc254e86e6dd13", size = 223585, upload-time = "2026-08-28T21:51:57.355Z" }, - { url = "https://files.pythonhosted.org/packages/b9/40/bede08621b1ba67e88c4d3336c22b52cb7911ff1fa4ef055344b6670e58a/coverage-7.16.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4080ad6bad9f14690e6b2104f5e8d137ccc65a4b5427a36662090637d4bd16d5", size = 254575, upload-time = "2026-08-28T21:51:59.233Z" }, - { url = "https://files.pythonhosted.org/packages/12/d8/ab0bdaa45dfd6b8cbf1a3ec548fdf827684b1997f9724375c5b3e89144fb/coverage-7.16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e9883a2f8206ce3af59117dc278e5d043fea06912bca3f199816129e5e2de354", size = 257172, upload-time = "2026-08-28T21:52:01.015Z" }, - { url = "https://files.pythonhosted.org/packages/1d/bb/135de81784bbd7dfedcab2b92b03d71d75b09b0815b42d6dabb052def5a6/coverage-7.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:984e5430fc6f858385009e92549955157d79335b1f3e13e1031e0f89d1284261", size = 258410, upload-time = "2026-08-28T21:52:02.76Z" }, - { url = "https://files.pythonhosted.org/packages/ad/72/ce44ecc062fb2e43d9447bb76154d091c2139232f20c125297c4b58f4c6a/coverage-7.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b1374099dd1ad0d31fbb6c95d00a56a3c5e85fb3343dca14fc12f78323a2b42a", size = 260539, upload-time = "2026-08-28T21:52:04.821Z" }, - { url = "https://files.pythonhosted.org/packages/e7/c4/9389c36a41e59406ca2bba493807c2294d2e5186a7e9ebcc2e63a0f2a711/coverage-7.16.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34d8686bce035c8465b318a8c2890e69ba14a00801a27f4eb6bdc97c23944d87", size = 254756, upload-time = "2026-08-28T21:52:06.68Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0f/7762447b15e01fb84263608540123c4d9941f06303265ee74d801ccbec0e/coverage-7.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:857fceba6ff4b507ee0ad98798a33d544a8473df0c542bf04251ee4ed5ee6292", size = 256540, upload-time = "2026-08-28T21:52:08.529Z" }, - { url = "https://files.pythonhosted.org/packages/e6/fa/c60dc75a8346c1dbebebc7279b19971c88f70dd575f0bc10bc0cb16f92d5/coverage-7.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bbf08d951abaa1ce89e28c998361d56b952413846b459cd017f116ad4c9adbfa", size = 254508, upload-time = "2026-08-28T21:52:10.323Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f0/4e0834f3a1fccaa8bf625a2a1d73bde0fa32577dc3249853c0dd0e7f2b20/coverage-7.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1a03e78f53e4d2ab13adac19958a89322d1829913e5623d642627bf60b35da21", size = 258659, upload-time = "2026-08-28T21:52:12.124Z" }, - { url = "https://files.pythonhosted.org/packages/b4/ec/fe712d3a11fd6e874565a5fa5497c48b8ece561d9611da040b44cdcf8386/coverage-7.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:dcd3dafcdd78305d27c59a1006b53a4990acb89e68d8fbe0992f4f83503c827f", size = 254326, upload-time = "2026-08-28T21:52:14.181Z" }, - { url = "https://files.pythonhosted.org/packages/e7/78/093e12072e01034c65ff380f76c74b79dd83e44fa92b689a2154389be734/coverage-7.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1bcfe470a796fbea6234accd81d258a31574dc0b7bf569e16be757572c4de17", size = 256102, upload-time = "2026-08-28T21:52:16.003Z" }, - { url = "https://files.pythonhosted.org/packages/9b/c0/265176117ca5d06e3f65575842884cdda96cf213350a31e9d41c80d65854/coverage-7.16.0-cp313-cp313-win32.whl", hash = "sha256:1420370276f1694b663207b8245c3628aafb9624fe3cebf313a13d860e55ee67", size = 225250, upload-time = "2026-08-28T21:52:17.82Z" }, - { url = "https://files.pythonhosted.org/packages/1f/01/8a87f2c04fde322430b45d16d8f543693e9894c5b2d2ca238a287c00beca/coverage-7.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:496277c8d7beed695e02c7be53516a0152e4caef8738a0feab6a638546cce449", size = 225790, upload-time = "2026-08-28T21:52:19.641Z" }, - { url = "https://files.pythonhosted.org/packages/23/40/c21feacd9edfe7063195bf9cc84d650e9938fc6a23063e4f027199b160e1/coverage-7.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:181c2906b9b3759955c1c33c51fbb91c754fbd0b82ea49e2c81061f5a052082c", size = 225180, upload-time = "2026-08-28T21:52:21.613Z" }, - { url = "https://files.pythonhosted.org/packages/ea/73/850675f262391b322c4c988b6cdc32cdc6629288f0fb158687b587a393a8/coverage-7.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:54b7fba6a74d010de34319a0419d5b65af8c00f539ad0b6f39fc6f342ab99697", size = 223258, upload-time = "2026-08-28T21:52:23.558Z" }, - { url = "https://files.pythonhosted.org/packages/61/c1/4f54c6d47c80d1cc58ef8fe6b74e6eb50f9e2c0f6e2de6cf38dbca2937b8/coverage-7.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fa4ff0b3dd52208d2b30903022d5087f82000507b504753dfeee83e4f32d6883", size = 223587, upload-time = "2026-08-28T21:52:25.627Z" }, - { url = "https://files.pythonhosted.org/packages/3c/be/298f2456230fb44e272a4e53a41b3f3c39f0821c242d7b7daa9787b4d6f7/coverage-7.16.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:35a9676bf86097f790113ebd9fb67681804ef54d40941d2f10ba68c02239e575", size = 254632, upload-time = "2026-08-28T21:52:27.689Z" }, - { url = "https://files.pythonhosted.org/packages/a3/9c/a1bda6439c19c4783d50df896142b67b9e7d432db36675d339a32778669d/coverage-7.16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f98d438add63546745e5e847192e3e9ab897ed6f2ca96f8281e2f5a15958ae62", size = 257139, upload-time = "2026-08-28T21:52:29.741Z" }, - { url = "https://files.pythonhosted.org/packages/f8/cd/cd735c9be757f97237c305f36897a5e5b348bdbc12ebed3b2b80060dd8a9/coverage-7.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:151855767480be14db595cbc2040f6a4db965cdfeebd354d79b0256742b029e0", size = 258484, upload-time = "2026-08-28T21:52:31.68Z" }, - { url = "https://files.pythonhosted.org/packages/e4/04/84b2e1e8aae9db3f549782f28ce25bba5fd6a9c7bfba3782ffe8b4cd2559/coverage-7.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:183613f664718b340589d7f005c7e92b4b601cffd20a8a4117cfda3e983b080f", size = 260798, upload-time = "2026-08-28T21:52:33.642Z" }, - { url = "https://files.pythonhosted.org/packages/8a/4f/e04cf52483619a4dc5dd6367b30c9a8ac52243567fdfacec9b11a441565c/coverage-7.16.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:785b114356c99c0dd5b3f57b9696cfd57b7704f4c53847df8dc88c6cc0d9bcb6", size = 254612, upload-time = "2026-08-28T21:52:35.543Z" }, - { url = "https://files.pythonhosted.org/packages/da/33/627c4113f66bfffd43807f54dbf080c4632ecf12e4ef7a3bdd4ec38e46a2/coverage-7.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:30f5aee6d1d517abcdfd4f9cad027969ff79a1440a22da263f9514e31b5b66e9", size = 256495, upload-time = "2026-08-28T21:52:37.485Z" }, - { url = "https://files.pythonhosted.org/packages/3c/38/aaca432f4e008a88f2bc4d1459aa7016d8d1bbbe801f7e4fa3cf2746557b/coverage-7.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:190ffa0f5af966254c249fb3aeaca2cef389785e3e287fd577d39e134d20f8a3", size = 254454, upload-time = "2026-08-28T21:52:39.425Z" }, - { url = "https://files.pythonhosted.org/packages/cc/db/8430aa87ef0a508f4c17c1b8fa7e0cf80231988d9081aa36c194036592d6/coverage-7.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0ccc37c00e1a5d30840902c54557e104d04aead872cedf6d2281c8725a467e06", size = 258728, upload-time = "2026-08-28T21:52:41.32Z" }, - { url = "https://files.pythonhosted.org/packages/76/88/cd8aa8c82493ffbd291d3ef5554452fffc634c6c6098a04ac848c79c98f3/coverage-7.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6c60cde430c0e7e3be612973af39b4cff90ec2e2defe7b2b701daea3a0ffff04", size = 254271, upload-time = "2026-08-28T21:52:43.278Z" }, - { url = "https://files.pythonhosted.org/packages/a8/49/fe16c811ea9314a84b48f34e4bf5a3d9013091093b285a74b2272fc863d7/coverage-7.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c5297028c8df849a61b29129cadfe682f90b5b396f528eb319a57d7678eefdad", size = 255927, upload-time = "2026-08-28T21:52:45.461Z" }, - { url = "https://files.pythonhosted.org/packages/d1/45/d0bd410e78cfbf768acc8099b335e1d5c0d5c26103c796d2bebdee001715/coverage-7.16.0-cp314-cp314-win32.whl", hash = "sha256:136988df5bc5a48795d9c42c75c4bbda5d9a78e750a080c1233010edff93a1af", size = 225424, upload-time = "2026-08-28T21:52:47.658Z" }, - { url = "https://files.pythonhosted.org/packages/17/78/1ce6ce4646822e9308dcdb1942eaf31bfd7da43247b8886338b0d6fe3767/coverage-7.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:ce2ba5e9f1842fe09165825abfb3bc6b527c71a27bc2eb3a10f2284ced64506d", size = 225918, upload-time = "2026-08-28T21:52:49.692Z" }, - { url = "https://files.pythonhosted.org/packages/f9/cd/e1323fe3a7dfcdd709451a43fe708ca1dfd36a7fc07b34eb7bd1dfdfb52d/coverage-7.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a89d07e48d9baead9a15599923a02f62c6df6c3d85aa84ef34be3c9fd6aeb91f", size = 225344, upload-time = "2026-08-28T21:52:51.665Z" }, - { url = "https://files.pythonhosted.org/packages/39/fb/1c15460d4cf915f09ae3ad3862fef4f901838991c5641b0cec545050d810/coverage-7.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6e2854b62601c89a63814ad5def3b90d99c6724cc4cb977f75b725e5fca4b1e3", size = 223986, upload-time = "2026-08-28T21:52:53.572Z" }, - { url = "https://files.pythonhosted.org/packages/9f/73/347d2d0009ac211f79ee2a2364fd2aa19d6b9628dc22ed13a9b9386097ab/coverage-7.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f093faf23df888518d273be6da65f0ec5a25b5d8b670231e4d87de07361042e7", size = 224254, upload-time = "2026-08-28T21:52:55.59Z" }, - { url = "https://files.pythonhosted.org/packages/5a/2f/51442e6ad9d705369596f08496021647e276d5b57311818fd4312d93509b/coverage-7.16.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b7dbbbf6551eb94618e7bc76ab61cc2740a5b3d13294171bd6adb36e12346c3c", size = 265619, upload-time = "2026-08-28T21:52:57.645Z" }, - { url = "https://files.pythonhosted.org/packages/ea/8e/0f752276f6d13efbd019ab6d90792e20d6272c44cda039dc5c6d27b91e7f/coverage-7.16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51e7d0e311d2fba3915f971236cbdd4ad821fc7a23988221c0b33c964b0eba22", size = 267734, upload-time = "2026-08-28T21:52:59.611Z" }, - { url = "https://files.pythonhosted.org/packages/fa/02/4df3baef8029881c9d1a380859f2be73f90080d430def567d182e8566a35/coverage-7.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bb04ee77e557d7476471969d35fbbfb5fc8a4152e9409aa5811780c36d9b23e", size = 270156, upload-time = "2026-08-28T21:53:01.658Z" }, - { url = "https://files.pythonhosted.org/packages/9f/30/ce10fdb74055ebbfb5c8a025d8845dc19c76e4b2c42bb5c755b56678990c/coverage-7.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c72c9b201dc0e8c2c8821d49858fd865010d08181bf877d2320971b6464ebfd5", size = 271279, upload-time = "2026-08-28T21:53:03.698Z" }, - { url = "https://files.pythonhosted.org/packages/71/19/c7e1fc9504d90da848493bad4018dd235c713a80633e48c5f0a41b63d45e/coverage-7.16.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0fca700cae4635656668ba6e2b66a85aac9f2622d7b2bcf82e844c409eaa1313", size = 264677, upload-time = "2026-08-28T21:53:05.741Z" }, - { url = "https://files.pythonhosted.org/packages/a4/f3/4021519dd41583ab396c81955387f927779641f6bac26818b6918a45aafc/coverage-7.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:584896fb8b650e999e24ef57e9513e482c12f8e15a73ee9d4584e23c99465867", size = 267610, upload-time = "2026-08-28T21:53:07.763Z" }, - { url = "https://files.pythonhosted.org/packages/55/fc/df65aac93938d8f506434c8e96440c1d696f6be0a6a01d3c6bfe5d49403e/coverage-7.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:949eae7e0f562b1518355aaef4b03523e49a6d3fea12aa3542d9e36c863f8267", size = 265217, upload-time = "2026-08-28T21:53:09.786Z" }, - { url = "https://files.pythonhosted.org/packages/32/2d/dc9a5e62715165fcb4c715f965f411e324917c9daeddde16536e9d36ce3f/coverage-7.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:64f0611ee05364fc85cc3e5bc371804117a76fd337720e6017332fc7c534257a", size = 268948, upload-time = "2026-08-28T21:53:11.866Z" }, - { url = "https://files.pythonhosted.org/packages/8b/4e/fe73a5560f25fca52acda76fc1554f30de081793ae4de97e920f8ab161d7/coverage-7.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:050a291b3cfe5e0df5999ef2fa5a7aff6e2db329f069d47eb63f02bde2e7e96b", size = 264061, upload-time = "2026-08-28T21:53:13.996Z" }, - { url = "https://files.pythonhosted.org/packages/b3/f7/bb78cc4b97085ebbd77fa18cbc25abfab462814efa3e2363b4e50885c775/coverage-7.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a336b1e2990a64f5c356a9b8380fb9c029d56c832b801255250c44d603271bfd", size = 266371, upload-time = "2026-08-28T21:53:16.233Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ec/84b4af5cd4ad498477b3bfb2217e47b048da919451053790efda66f7383c/coverage-7.16.0-cp314-cp314t-win32.whl", hash = "sha256:058631257350b31784ed43ceb808298b6f074edf4ebca4c7ce5082e6bf873a61", size = 225736, upload-time = "2026-08-28T21:53:18.632Z" }, - { url = "https://files.pythonhosted.org/packages/7e/43/50fc0e6c675c3ef14895a74bab2d6120cb5d6f4b562a3d3f5046797758dc/coverage-7.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ed35097438dfa980c1ec75bc83edf8acbe7a374d7007e571957a257fbd0e2fb3", size = 226570, upload-time = "2026-08-28T21:53:20.754Z" }, - { url = "https://files.pythonhosted.org/packages/fc/24/9effce7bcd3c6eeb4da3561905837509e582dcdde7a7f07d6ef2c8512f76/coverage-7.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0466f4a5c0370461b7d8c7eb259d7d1db0b5756f13d66230b04d22a1d380ee11", size = 225879, upload-time = "2026-08-28T21:53:22.747Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5a/234e8fadf85c3cc48cb31c247b9e8e0c7f06ece80f5b29f9b8c241f9da4c/coverage-7.16.0-py3-none-any.whl", hash = "sha256:245f7de6d023a5bba375dbec9f2e0869bfa26ac0cc639bbb7b4c814884000b73", size = 214977, upload-time = "2026-08-28T21:54:35.189Z" }, +version = "7.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/2d/c738872f477f5687152acae68635790387425d407ae37dd3d3a8a6692307/coverage-7.16.1.tar.gz", hash = "sha256:f83981779bcf9dfa06fa0a8d4cb43e0faec1706328ce07aa3e7b665b4ac0f210", size = 969651, upload-time = "2026-09-13T19:12:21.422Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/f7/7cd4c9f2a3b7574414222ec425d5eee21cc690d867a013315e3be8ba185c/coverage-7.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b7f2c26ce6ce0b1e0ca0d5fae96ea510e3a2e78b7207f06e76b7f2c87fa3d0af", size = 223475, upload-time = "2026-09-13T19:09:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f2/9ac65f9cedd43f91e2d3657c11f13aef82aebae89b2243dc9e44fe58a740/coverage-7.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070acb9da788dff743a4d36fc015feee12d68f0349959017542017c79f59c21c", size = 223845, upload-time = "2026-09-13T19:09:11.988Z" }, + { url = "https://files.pythonhosted.org/packages/62/4d/f13db452d4367fe1122abfd98ae330596f65a3b40498f8e1a5811f68f123/coverage-7.16.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e366587b370bc9b8b51b7b7272c610c56db5d5b4795b9e4a29d28ff2f440f809", size = 255341, upload-time = "2026-09-13T19:09:13.708Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6b/85ed86e82835a96ddfbe7705c387c28ce1cbbecd1b6d606f5ddfeeb712df/coverage-7.16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e82e10b9d290f60b63459cfb245a841aec347603997206296b93881463a93dcf", size = 258078, upload-time = "2026-09-13T19:09:15.368Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d2/f66945853d850b9c05f4e012e37396f1f31d0cd40d062394b229c22e7b56/coverage-7.16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d73bb1f85c4150ac208fb0755beb04b2e44897bad81414de9380f98dd74729f", size = 259191, upload-time = "2026-09-13T19:09:17.046Z" }, + { url = "https://files.pythonhosted.org/packages/77/a1/7b907abe62461f289035c7ac60ab3e6334efb8c425151aac4abfa0c97820/coverage-7.16.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3397b9032553d281ad6a9253b12675b65e0cc8cd7a3b0633cf48872c9eb13360", size = 261452, upload-time = "2026-09-13T19:09:18.756Z" }, + { url = "https://files.pythonhosted.org/packages/51/e6/e26f4d6b1069a2a2fee3238a132f85c5b4c1dc25aebd88e015451ff0bd68/coverage-7.16.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77890395cf37026a5907d3ad32376aa51f41c0f163b7477fdbd4f94966cc1d08", size = 255698, upload-time = "2026-09-13T19:09:20.786Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ec/99db7450e813050ebce300e31bbd4f997c76a8c6e3a6d168c5dffc104109/coverage-7.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b0944dc3bee3091039bf970d73caaf930c906013128a421bdc132e797494d941", size = 257111, upload-time = "2026-09-13T19:09:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9d/500df9d3cd8c541ac84b5e0bcab00646be75654e05aa34e8410ec851282d/coverage-7.16.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b89d22a89d5bc05dd95b64e08295b8394aa96dc88e08f8ba210c9ebfebbe0489", size = 255258, upload-time = "2026-09-13T19:09:24.429Z" }, + { url = "https://files.pythonhosted.org/packages/c0/24/53318d96da332bb4946f8ac646fa19fd37fecbd0c49144735c716ac69bf0/coverage-7.16.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:550a2a1faf7559f13d5344f12d1eb886ad87955155d7dfab2a3fe5c8ec8fe776", size = 259326, upload-time = "2026-09-13T19:09:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ce/abc0462b2e6ae96ae22197d2bc30fe33b6b4e52937e782745436ceb2a761/coverage-7.16.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:55eb268e5b81aefac759766c9162625b06c1bedb7b77d936225bafc4f038a6f6", size = 254827, upload-time = "2026-09-13T19:09:28.191Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e1/0a04eedaaf19196b51f0180968134228c7646e469ed29914e48690a7cf3e/coverage-7.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65a8fc80898c9ce59f04349fe8b4849b1f9787f14e52ead990e5f849ff4727a0", size = 256698, upload-time = "2026-09-13T19:09:29.922Z" }, + { url = "https://files.pythonhosted.org/packages/de/1d/d441a55cf22ce9d8e9e34814806c47441ab844bd534e0f4b64e1c6ae8bd1/coverage-7.16.1-cp312-cp312-win32.whl", hash = "sha256:528a61be40977c340cf201d23b69bd6a6bab507da60e9dbda85f8b30e935d70d", size = 225541, upload-time = "2026-09-13T19:09:31.752Z" }, + { url = "https://files.pythonhosted.org/packages/73/27/ec3d032375735dd331477caa051419678079ff90fcb53d0284a6c2bfb757/coverage-7.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:d0f02c633630e2b74522108ee95a84ad6e1204a8016a6cca5297f335ea27147e", size = 226075, upload-time = "2026-09-13T19:09:33.556Z" }, + { url = "https://files.pythonhosted.org/packages/94/00/90e9f5c4434878494306b9c0ee8068ebbd829483737d8cefccc58884d728/coverage-7.16.1-cp312-cp312-win_arm64.whl", hash = "sha256:2959978f9d1d20a2c0c15d0a68baaeccf615ac1aa214cf4a05a10d6f568926c8", size = 225461, upload-time = "2026-09-13T19:09:35.48Z" }, + { url = "https://files.pythonhosted.org/packages/aa/74/c08c0c4dc9fa6bcd1d90728a63660aa1b17b488a806948598456c48f75d1/coverage-7.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ee5465db6e9152a7d09f3215309326878c6aa3ac509195a369f9d264ff4bfbd9", size = 223501, upload-time = "2026-09-13T19:09:37.276Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c8/784986d326663285258a8e39835c46fb730cc85284f0dbcd82078586dd22/coverage-7.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b8256f8b525ba233d2e4cdcdce0d6673c66fc9bf70df1fd5e67c54a74e2d245", size = 223876, upload-time = "2026-09-13T19:09:39.385Z" }, + { url = "https://files.pythonhosted.org/packages/15/76/73fb792928872bbb07e553f920ff55c65ee962c469265feb5d1d4ae5f97a/coverage-7.16.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57cc400275b9a2892e905fc893f732b21ddb95271bf96406c88e2f6367848b5", size = 254863, upload-time = "2026-09-13T19:09:41.326Z" }, + { url = "https://files.pythonhosted.org/packages/ca/8d/1fd78899513244b065ccecdd1cfc6aa8b8f1bdee796d8c4f2aa8e8e5397d/coverage-7.16.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b3fd0f3435ebb7a7183b32a6062a8b755f08242ced1f3f22761d30b56b3c2a5", size = 257460, upload-time = "2026-09-13T19:09:43.086Z" }, + { url = "https://files.pythonhosted.org/packages/47/1c/b23ddfcb7ef9bd7b3fdb7be9a5dccf9925ae88e556df7aa5b655283c78f2/coverage-7.16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5eb1762e7eb5fad34ef913e8107c7788a66f19d328e598ce95bf7217f9e5c8f", size = 258697, upload-time = "2026-09-13T19:09:45.161Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c9/19d0c6ca35778a7e9415c30c46a0c64c9d4374219d004a35563132296896/coverage-7.16.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46cd3a73e9140410de62cceb66214bce0e08fb3922b9176fbfc1522fec151b41", size = 260827, upload-time = "2026-09-13T19:09:47.061Z" }, + { url = "https://files.pythonhosted.org/packages/97/fc/5862f7344382c62b85df43d483e579168cc062e007f54d895dfa51fe3e7d/coverage-7.16.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1ba5142d68dd2cb775cbd0ac8601819298152047803c8efe4eec6d7d7aa7878", size = 255038, upload-time = "2026-09-13T19:09:48.945Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9f/5c26583199a68df8d15124b4590520903f8eb7cef17db293fea712bb0783/coverage-7.16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c389c6f9d1d518e1249ddcb8a7f158135644ce2c508fa6cc17b680777dad5bf2", size = 256827, upload-time = "2026-09-13T19:09:50.738Z" }, + { url = "https://files.pythonhosted.org/packages/95/13/605198bff079b107336710f28a797312ab132587168600d70a290e7ecd2e/coverage-7.16.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cdc57746c7ac0ea063351b4d651c3bb4dd4fd35e64dbb8e90c10e14eb03c4080", size = 254794, upload-time = "2026-09-13T19:09:52.577Z" }, + { url = "https://files.pythonhosted.org/packages/43/b7/0bbb32dc5ccdac766a13763fdfbffd7d73fc80349a69230c46c6b71e7508/coverage-7.16.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5597180ed7670cc94c04c65347418a467d3a43d5f0cf52fcac647f5425f42037", size = 258947, upload-time = "2026-09-13T19:09:54.372Z" }, + { url = "https://files.pythonhosted.org/packages/13/bd/65c31ddd43ff4e61b63721b3dad17cca412dba6e2ce89f92abdf75734339/coverage-7.16.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9647a0ac46255b8fef59a433a2161f03e5483f3a35e1cbd9dfe4600baff0c6b", size = 254613, upload-time = "2026-09-13T19:09:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/25/20/d278115f2ba522b3e3128c6852be265f4e0df7202b2effca4db96cf0217d/coverage-7.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e306e98186b9cd109121f3583aeb7978797ad21d948f22944c5c08845cd554d0", size = 256388, upload-time = "2026-09-13T19:09:58.095Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ca/d43aa396fb3a2f71c9411b99d926475f5acaf5c124f605c592865c80c8d5/coverage-7.16.1-cp313-cp313-win32.whl", hash = "sha256:48a78a66fcce49d7f6156524bf979c0ac633d584199717c68c6ffa949fc14e6a", size = 225550, upload-time = "2026-09-13T19:09:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/0e/30/df2f6114f6a17cffe94721bd1d298f77f86aad493717fa5bc03cb291a1a6/coverage-7.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af03247d598a353bbbbe1b925deb735276e4d845e7197c4073dc89352b236fa", size = 226091, upload-time = "2026-09-13T19:10:02.331Z" }, + { url = "https://files.pythonhosted.org/packages/ee/31/6f90d8aab72a112492bd50e3b5485b53b772b1f5fa70da0a620e723caae6/coverage-7.16.1-cp313-cp313-win_arm64.whl", hash = "sha256:166adae25b05b04c9a84135912066d9c97482115af38df1a419a38aacc6b6f5d", size = 225481, upload-time = "2026-09-13T19:10:04.151Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b4/2a7c793965bae9f067aabab793a44d7a2f3ee7fb16b01ce1976bbd4a0218/coverage-7.16.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cc0b37fe6f5ce5f1ccc62ad4fa9b1ad201d8e9b6027fd5e0170877beee4b2d15", size = 223546, upload-time = "2026-09-13T19:10:06.019Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e2/633469076a2dbbea036cc15a268a3a5d6b2c7dd5d9a9567b2553dfc5ad61/coverage-7.16.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6618f481053b63fc6121faf8fc676bd9b7163c2a19d9e984a2e850002c28ab57", size = 223881, upload-time = "2026-09-13T19:10:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/de/c3/f06150c13284569d53273b909f31222874276a595637b7852571dfeb2c18/coverage-7.16.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fa02d561eb1d8d2f8ba43ba6e3cef4c6c402a3b632a9460fa329fcadcd5df6a3", size = 254919, upload-time = "2026-09-13T19:10:10.254Z" }, + { url = "https://files.pythonhosted.org/packages/d5/40/47e25b215ae18a29010c8e29be8782a6e04d18ba6224be2bf6cebfce6427/coverage-7.16.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc5354a124799f1f87b7637bbe6f18cd4bc66a1f37f6aa2b5db40f9adad531dc", size = 257428, upload-time = "2026-09-13T19:10:12.124Z" }, + { url = "https://files.pythonhosted.org/packages/27/4b/1e2a4267d14cbd12a8489364a9d40020233e6be836d929b363f0e77209e2/coverage-7.16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34bafe9f4094315248573e6223e11af0ec1b25f9cbca43bf0e9a26a189ba2751", size = 258771, upload-time = "2026-09-13T19:10:14.031Z" }, + { url = "https://files.pythonhosted.org/packages/be/2e/9aa6146cea929fab9185bb2642ffef7f47520a6e5efe407f75f9b12f4cf0/coverage-7.16.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:29c4d3e32a3b5efa420a3dc627c7e570deb80ef997def52c7686a474f5edc7ab", size = 261086, upload-time = "2026-09-13T19:10:16.213Z" }, + { url = "https://files.pythonhosted.org/packages/13/3c/f9ad8bcd4fb3d21c9d20a16d6d6c6f999eee8f4498ed7659a3dbd2f4b74a/coverage-7.16.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2066c447fdd0bca39a9633a082d8ce67bf9a539a203b85059a364a405dc9fe9", size = 254895, upload-time = "2026-09-13T19:10:18.602Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d1/47eda9fd1eaeea39fa7b5b13a63b2bed92ab901841fb120b3f9f5e1dc30c/coverage-7.16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd8ac10cd2458b3c6343aac082fb9bd0e3fa806cb2c4975f2280153474b88412", size = 256783, upload-time = "2026-09-13T19:10:20.778Z" }, + { url = "https://files.pythonhosted.org/packages/38/c3/565edf044877cb8cd3373c56885347ffc38f0edfd1f1679a487b208c19a8/coverage-7.16.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d8c54ec32e5c102b9241f75d88ae26538b53662868ca491736611db448d9c7a", size = 254742, upload-time = "2026-09-13T19:10:22.733Z" }, + { url = "https://files.pythonhosted.org/packages/fd/88/87d2b2aeaba719192b2089ff1c2cf89a06cf73a6d2e9f1f145626617700c/coverage-7.16.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6dd8dda3402a01a1a8fe8b753a282466f615128574a5590a9108acd07b1f8540", size = 259016, upload-time = "2026-09-13T19:10:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1b/70813185b125768abdcf7899fec4d37edc2e5fc9b60c7045c8f4271ec757/coverage-7.16.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:79afa9726438912e5cddd1fe541815cea9763c92935f594835e4c432565b68a9", size = 254559, upload-time = "2026-09-13T19:10:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/d8/fa/e7aa5af279aafda633a1ede8bfd7d6916b0c8b2082be86759e0b52e73a61/coverage-7.16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3db3978211c3cead5437a80136ca0556bab8bc7828de15a762884b0598c41361", size = 256215, upload-time = "2026-09-13T19:10:28.714Z" }, + { url = "https://files.pythonhosted.org/packages/38/87/7a894fa4f8c6662d2b6a87a3436950e15b1fa56e01765c9d6634fb2cbeb8/coverage-7.16.1-cp314-cp314-win32.whl", hash = "sha256:49c39c7068a494f8eb427155f5682f44feee43f9b3107fd54b1e52465379c54b", size = 225719, upload-time = "2026-09-13T19:10:30.743Z" }, + { url = "https://files.pythonhosted.org/packages/8b/01/fa7193c8005fb85488f02b0e1cc3c05a233cf2640206dd978af447aeecbf/coverage-7.16.1-cp314-cp314-win_amd64.whl", hash = "sha256:c510dad19552d912058e4c3e3cbec3fb155dbe8d0ce0ceb7e7dbf5c5822bae0b", size = 226208, upload-time = "2026-09-13T19:10:32.698Z" }, + { url = "https://files.pythonhosted.org/packages/da/5c/a08634c714924c3eaef811bb3576c044128aa5e7dfa86c75e52f0761849e/coverage-7.16.1-cp314-cp314-win_arm64.whl", hash = "sha256:b7d4d7e6dcaf33e85f1919f03346403bdcc27437c420a78835f3805bca0ab71f", size = 225633, upload-time = "2026-09-13T19:10:34.79Z" }, + { url = "https://files.pythonhosted.org/packages/43/df/ddb8a4c664046b1a0ee29c9c2d25b993e5dbc8fbde715df3694a64532781/coverage-7.16.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3d0a3681c12d3e0bcdea3d9414b04087828d6c1a482802d6f7f42c37ed530152", size = 224281, upload-time = "2026-09-13T19:10:36.853Z" }, + { url = "https://files.pythonhosted.org/packages/e2/d0/9076e0c762d8afd91182e60a520fa5c92c4a334785eeb9fd6b8ef8fe7e3c/coverage-7.16.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f3b4469d3da3ecced775d1a8c9c5d9fc80f259e30b7b89f9fed0700d6035ecb", size = 224547, upload-time = "2026-09-13T19:10:39.359Z" }, + { url = "https://files.pythonhosted.org/packages/03/e5/9c59e64b6161704f35fe91549bb19b2bb355e95caf596c26a2065564807c/coverage-7.16.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c08ae35c1be2fe1ce4b4c628df5c6fc0dc9a87f8e5fe8e20238d249678984741", size = 265906, upload-time = "2026-09-13T19:10:41.434Z" }, + { url = "https://files.pythonhosted.org/packages/57/5a/13ccaffb77f766101bf6f38be9dba9e468b02cc92da4552a57877dbf1c1f/coverage-7.16.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8ee71a38c54bb2676bbe762b8b0943a79ccb1c2fd6a52054f66e63eda392f8c1", size = 268023, upload-time = "2026-09-13T19:10:43.533Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a1/05cfcf01d3c7c922832698ad46e51d3441d820ce87a943014bb5cf5710dd/coverage-7.16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76491917771f179f9772efe218c5ccc65950dbdb35f4439298d8a8dfc6ec1f72", size = 270442, upload-time = "2026-09-13T19:10:45.895Z" }, + { url = "https://files.pythonhosted.org/packages/72/15/a2f1544b8e3835d7b769f7dabcc9ac0283e0b646ef3344703ff8f18d83e6/coverage-7.16.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4aa0b0a6f81fa3deb211e643f6954e78b4376b62b9c218271236cfa757664e8", size = 271565, upload-time = "2026-09-13T19:10:48.123Z" }, + { url = "https://files.pythonhosted.org/packages/df/5b/963c2993a82bd313f298d663afe03e164b96ace4d9d4c7561740a559e13d/coverage-7.16.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:756ba2d96d073c5a2a55d67fa22784763710fadbe22c41adde2d9cfa4dd78a8c", size = 264959, upload-time = "2026-09-13T19:10:50.195Z" }, + { url = "https://files.pythonhosted.org/packages/12/59/5eba06d1943735d7cd61d46d8c8a20ffe8ddd2da06b3c94366078dadeb9b/coverage-7.16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:99bf9ea435cefcefd220f8687c3ddbbf78dc2de0bd11b57c3ae9fbbdf8d5561a", size = 267897, upload-time = "2026-09-13T19:10:52.252Z" }, + { url = "https://files.pythonhosted.org/packages/bd/48/af6c30f6ea431bb9b83f9070d268a9cc4fc97490abd32080164177ea999f/coverage-7.16.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:35cbc81f937fc402971df45c897d2df2bfb2014efcd990360032aa0a651635da", size = 265504, upload-time = "2026-09-13T19:10:54.432Z" }, + { url = "https://files.pythonhosted.org/packages/80/f2/6e13852a8656d05fa83284567dd5a5b1e6d89bef79fe3effca2787159eab/coverage-7.16.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:8fae08e85b334ac6ac886002b5041396a31bcf805225bbe19847627203da99e2", size = 269235, upload-time = "2026-09-13T19:10:56.563Z" }, + { url = "https://files.pythonhosted.org/packages/c2/32/b4fe465daa64ece674f83a750dfa4ba0fa3c5c74d6ef5dbb8dfce892cf0d/coverage-7.16.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:83362b64e215ef00b0ba33fcf13655ace6c9fdd144d5ad2ab59ac86c2daf166e", size = 264347, upload-time = "2026-09-13T19:10:58.634Z" }, + { url = "https://files.pythonhosted.org/packages/54/f3/88b5c0e4ca3994c6d5feb7b1bf4c9a62cee205553159184968426930a7b1/coverage-7.16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:33300f2e140ccf26af3d8152e62bff71993f9310cfc63ba7a20940b0d246a0ae", size = 266660, upload-time = "2026-09-13T19:11:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/97/72/6eff5456d7ba7f1c4678af531c33f9d957cae3201bd229b056fd13a204a3/coverage-7.16.1-cp314-cp314t-win32.whl", hash = "sha256:5539304fdbb2cc144df684d35a33b81145334d23e1c2367b5a923d25107f70b2", size = 226026, upload-time = "2026-09-13T19:11:02.846Z" }, + { url = "https://files.pythonhosted.org/packages/8e/c8/6e5ae3d8d4d0f2c0078985bf4db55fafd90e8107b1bf91ee3547a13f5694/coverage-7.16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:715dcb72c3280c428c3a20134b87e42c29acec9669136e899ab2de69ca86218d", size = 226862, upload-time = "2026-09-13T19:11:04.921Z" }, + { url = "https://files.pythonhosted.org/packages/be/c7/68f9f0734afc904a92b974b489545b6a15700f3b1c4bd36eae764561e661/coverage-7.16.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dac8b84c03e6029d272b8249c77018db83de59ca009a9adef7c144b4a62ee5e6", size = 226171, upload-time = "2026-09-13T19:11:06.969Z" }, + { url = "https://files.pythonhosted.org/packages/96/1a/d6d16babd0a5fe4c3fae40702158c570351694e74516d8d81b86c5637448/coverage-7.16.1-py3-none-any.whl", hash = "sha256:3d8bd4e58b6a5c2018d808f297905393c6c61da466a48c3f0596a76a4900ebe4", size = 215264, upload-time = "2026-09-13T19:12:18.895Z" }, ] [[package]] @@ -1350,7 +1350,7 @@ wheels = [ [[package]] name = "google-api-core" -version = "2.36.0" +version = "2.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth" }, @@ -1360,9 +1360,9 @@ dependencies = [ { name = "protobuf" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/d8/88c2f0e6b0dd46a7796cca64fad99c7adba2417916f5393e82b9b7d2548e/google_api_core-2.36.0.tar.gz", hash = "sha256:32779307b52e64c9a9592a3621de6281676ecaeea299fe8524e4637ab7ac2531", size = 196879, upload-time = "2026-09-03T22:30:51.216Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/19/0a08cdd1198b3b74550464b8108f6976e873f0b4b035e1be3c3872345153/google_api_core-2.37.0.tar.gz", hash = "sha256:cf58f220aa797f1ffdda52194c4c7d72efeced09297c2976529f8135f6d85b9a", size = 203563, upload-time = "2026-09-14T18:52:47.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/56/30c91c61b8f70d4c09285a005b94798729aeaf4ec8b90c1c360da8207728/google_api_core-2.36.0-py3-none-any.whl", hash = "sha256:e4d0b179260727ea5c42222426d9199285214dbef7bf48f8b16600c7f9a78944", size = 184118, upload-time = "2026-09-03T22:30:06.662Z" }, + { url = "https://files.pythonhosted.org/packages/3a/35/6db89f5aeac2f38815105e5dab66f48a0ff035aed6e4a86c81ab0ff483bc/google_api_core-2.37.0-py3-none-any.whl", hash = "sha256:d84042a0034cce9c4304e17d2e5e9966c2fcb1a851b73799291f687a67470c8b", size = 186342, upload-time = "2026-09-14T18:52:24.064Z" }, ] [package.optional-dependencies] @@ -1418,7 +1418,7 @@ wheels = [ [[package]] name = "google-cloud-bigquery" -version = "3.45.0" +version = "3.45.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, @@ -1429,9 +1429,9 @@ dependencies = [ { name = "python-dateutil" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/a5/64ba829eb6c8acf5c0a80b4c9b217c286a2cd592f661121660de9cdba329/google_cloud_bigquery-3.45.0.tar.gz", hash = "sha256:5a799856825f47743ce561802cbd01b4d5c70062b36a942cfef5146017ecf0a4", size = 528642, upload-time = "2026-09-03T22:30:57.289Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/f6/e12217ae9518d35e014b2302ddd3a7e047c0854c692e536395c12eb4f81c/google_cloud_bigquery-3.45.1.tar.gz", hash = "sha256:f89bb9d1d546b7495a4c3baaea4f2d56c336a4e06581a19acd8eb8547266e591", size = 528170, upload-time = "2026-09-14T18:52:49.522Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/af/f1c877f3d47a616f19003ba958cec5efe6e8d43d3576c5827bf53ae94ad2/google_cloud_bigquery-3.45.0-py3-none-any.whl", hash = "sha256:30637314d67526cbaefebfb9106c5a41c5f30a137ace404531148f09a6b487a6", size = 267964, upload-time = "2026-09-03T22:30:15.283Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ce/89915c377eaffca9d509e64631185254fa8e07f0fedd3fb749a31f9f7915/google_cloud_bigquery-3.45.1-py3-none-any.whl", hash = "sha256:d58c1b77b7c5ae1a405be7c5341e78ebf61f352173393cf35ef722c1432e6a73", size = 267691, upload-time = "2026-09-14T18:52:27.798Z" }, ] [[package]] @@ -1556,49 +1556,49 @@ grpc = [ [[package]] name = "greenlet" -version = "3.5.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/d8/7cc97c142388aef03f622e001c572c4f84e9252a439549d483f555771970/greenlet-3.5.5.tar.gz", hash = "sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c", size = 207585, upload-time = "2026-08-10T15:09:36.136Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/7e/9ecd0285e3153532ae07aeb88063c43c72b4221cf0d4d123b02f3682e3ff/greenlet-3.5.5-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380", size = 295809, upload-time = "2026-08-10T13:25:34.023Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/60e4bbcc89252037b18087f2ec16405d5b2d5be42dde191bbf3667e96102/greenlet-3.5.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053", size = 611910, upload-time = "2026-08-10T14:14:35.18Z" }, - { url = "https://files.pythonhosted.org/packages/a4/17/cd5134be659cd4a443e7a61ae670dabec165a814c51162916d637b6dd38e/greenlet-3.5.5-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95", size = 624198, upload-time = "2026-08-10T14:27:25.229Z" }, - { url = "https://files.pythonhosted.org/packages/9b/30/87c212b5c684d0e72974f1063b7a9687631e8985902c06e1016542c874e7/greenlet-3.5.5-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ca5d6ae0739e5764f2cfcfaa562ac5a990cbdaedca93251c5e3cf07c362371f", size = 629504, upload-time = "2026-08-10T14:30:07.967Z" }, - { url = "https://files.pythonhosted.org/packages/78/ac/5c5b959999b6f09c3026b5dfe171575bc3121c5236ce74f495096f25b203/greenlet-3.5.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d", size = 621439, upload-time = "2026-08-10T13:40:49.391Z" }, - { url = "https://files.pythonhosted.org/packages/63/2c/eb487fafc9f50ffff2b1e0b697f70fb34bf150821c08ab225aacf5583a7e/greenlet-3.5.5-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:1b5ed9162c0c098e0bbc2cf88a94f433c1b8926f831745252e099e5d83e17759", size = 432462, upload-time = "2026-08-10T14:30:02.309Z" }, - { url = "https://files.pythonhosted.org/packages/c8/8b/6acf112ed8aee499f25b4d6949820fb02ac950ff9c1f3d793bd5be0599f2/greenlet-3.5.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b", size = 1581342, upload-time = "2026-08-10T14:15:05.653Z" }, - { url = "https://files.pythonhosted.org/packages/b8/d7/734e5f198888876b42d7616ff6644c075baf6b8a2412deadd6b0e1b8b20c/greenlet-3.5.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2", size = 1645744, upload-time = "2026-08-10T13:40:30.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/30/1f42b88dc587b5899ee50616ad56ee40cafaf225df4fb829f10183c62a5c/greenlet-3.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18", size = 324171, upload-time = "2026-08-10T13:28:44.472Z" }, - { url = "https://files.pythonhosted.org/packages/76/e5/4dee4d8d2e603fe5fdd7b444e63219f7b9bd852c60c6214511c7157cbe88/greenlet-3.5.5-cp312-cp312-win_arm64.whl", hash = "sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52", size = 308362, upload-time = "2026-08-10T13:26:46.839Z" }, - { url = "https://files.pythonhosted.org/packages/fb/3d/8cef5f724ec0d4add2af8961d504535ec60c3cca9e464f6d03bdba29d85b/greenlet-3.5.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa", size = 294730, upload-time = "2026-08-10T13:27:51.206Z" }, - { url = "https://files.pythonhosted.org/packages/88/4b/8e7aa3f514273aecff30a16ab1bac09ff54cfc7e6860fdd8058c37ff2499/greenlet-3.5.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed", size = 614536, upload-time = "2026-08-10T14:14:36.589Z" }, - { url = "https://files.pythonhosted.org/packages/85/48/4e95e9dd5a8a397dc6a6345dd7f1935113d0fca4f85e89d3976da9cd988d/greenlet-3.5.5-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef", size = 626924, upload-time = "2026-08-10T14:27:27.048Z" }, - { url = "https://files.pythonhosted.org/packages/0e/84/eaa476d6bf3816828d0d70e80dcc36bf30a058233bd889e707e693f6e860/greenlet-3.5.5-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7278591501941bb2456af102bb9cd59aab48c6cfd6e2dd68fa1290bb0c49a42", size = 632726, upload-time = "2026-08-10T14:30:09.874Z" }, - { url = "https://files.pythonhosted.org/packages/89/5d/398a1c71fa7a277deeb376c999979de6786f08fc2d5747a0b9d6e11738dd/greenlet-3.5.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0", size = 623906, upload-time = "2026-08-10T13:40:50.501Z" }, - { url = "https://files.pythonhosted.org/packages/d0/f2/0cc2849ede68579291e9c59b3ab6ec1958f98681cca5b14d8fc75bf674a4/greenlet-3.5.5-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:4dfc7c4470354e7b09184d1a3a985761053a2fd694ddb5b5c80242afc2c8c90b", size = 434966, upload-time = "2026-08-10T14:30:03.729Z" }, - { url = "https://files.pythonhosted.org/packages/04/1b/745450fc5ea9e0cb17d840d248f284db3363de736d362c7d2d883e3eadba/greenlet-3.5.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537", size = 1581430, upload-time = "2026-08-10T14:15:06.853Z" }, - { url = "https://files.pythonhosted.org/packages/d4/29/d51b296e3191bb15d3d81ec375af1909e4466c0f395d744ed475801798a9/greenlet-3.5.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e", size = 1645684, upload-time = "2026-08-10T13:40:32.133Z" }, - { url = "https://files.pythonhosted.org/packages/12/63/369f1a1625e64e9e31df3963c6044056e3fdfa3fa3fdba3c54ffefa6e987/greenlet-3.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd", size = 324075, upload-time = "2026-08-10T13:26:58.974Z" }, - { url = "https://files.pythonhosted.org/packages/45/78/649cb5c09d4d81f6dd1444e75474a7206784743283a21d24171562ac4899/greenlet-3.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc", size = 308260, upload-time = "2026-08-10T13:27:50.795Z" }, - { url = "https://files.pythonhosted.org/packages/7f/8c/080e881fa2be95ff1ddbd6994b2bab3b1a78df3b3fcab39306011764fcc7/greenlet-3.5.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d", size = 295309, upload-time = "2026-08-10T13:26:03.032Z" }, - { url = "https://files.pythonhosted.org/packages/25/cc/0ac614e6586c0e42d4cc281a5819150f4f43685744a4c5ff77139286409d/greenlet-3.5.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328", size = 661185, upload-time = "2026-08-10T14:14:37.867Z" }, - { url = "https://files.pythonhosted.org/packages/5e/b9/6808725354be8ad305dfe5172377664fc9642d4fc043be246b3314cf4482/greenlet-3.5.5-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926", size = 673419, upload-time = "2026-08-10T14:27:28.652Z" }, - { url = "https://files.pythonhosted.org/packages/eb/52/f005d579acde46c3d1cc3cab1c9f3d5708c8a3006a4120e8cf5da801afe9/greenlet-3.5.5-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d98ef6f92e67c6dbf299dbfd8facc1b0d2d9cedf91e325e73b3d0373fe4309d8", size = 677863, upload-time = "2026-08-10T14:30:11.663Z" }, - { url = "https://files.pythonhosted.org/packages/42/2e/40c509967da7f254680826a2fa0dd22138ec79946c70b97542d74cde8b43/greenlet-3.5.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e", size = 670822, upload-time = "2026-08-10T13:40:51.833Z" }, - { url = "https://files.pythonhosted.org/packages/c4/8a/a75f8a2bdcef3c358a3147cdc9db3aa83755f0a038f766ab0bedb66f512c/greenlet-3.5.5-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:159df1942d88e8f784cbb38d6f18bdb365cd11319cfbb3e89623de2b97892d53", size = 480554, upload-time = "2026-08-10T14:30:05.171Z" }, - { url = "https://files.pythonhosted.org/packages/2d/22/c3c2eee4a8fe191d6d1d183086c56133d646024e3d70bfd414829f64560b/greenlet-3.5.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc", size = 1628469, upload-time = "2026-08-10T14:15:08.11Z" }, - { url = "https://files.pythonhosted.org/packages/f7/87/25babd09b94cb1f03e71db815fde463f0262e40cfbd953d58a8d77311351/greenlet-3.5.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9", size = 1691952, upload-time = "2026-08-10T13:40:33.502Z" }, - { url = "https://files.pythonhosted.org/packages/2e/3d/5cc9701117ea4dc0eb7bf1f4f9b7888a6e2e5277ddfae095805ace50f2b6/greenlet-3.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1", size = 327458, upload-time = "2026-08-10T13:27:02.868Z" }, - { url = "https://files.pythonhosted.org/packages/a7/6b/594fa2de7fae7629168a404a4305d7d7e31a5742c50a801b1839543cb93d/greenlet-3.5.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07", size = 311146, upload-time = "2026-08-10T13:27:25.046Z" }, - { url = "https://files.pythonhosted.org/packages/24/e0/50cd600b469e5734c72709b6b1838b6bc63f307b573c772c3132d6ecfe92/greenlet-3.5.5-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277", size = 305471, upload-time = "2026-08-10T13:26:20.568Z" }, - { url = "https://files.pythonhosted.org/packages/75/a3/77acd66dfc6387b5219b2080806c0cabb73c10eb1bb44b413c40a62015ba/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b", size = 672470, upload-time = "2026-08-10T14:14:39.058Z" }, - { url = "https://files.pythonhosted.org/packages/b9/71/0d178142dca3ec19f46fb2212ae73d30ad53b9d548dc64804086033a7089/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272", size = 679973, upload-time = "2026-08-10T14:27:30.072Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ac/0d7887aa4bbfc9eba075cc428244dfc96f623478454d5ec81180d0d6bd5a/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e9ec2e7c98e895fcea0c5cc57b2606cf86ece6d0a56578f3eb225e2af4f0387", size = 681587, upload-time = "2026-08-10T14:30:13.519Z" }, - { url = "https://files.pythonhosted.org/packages/6e/31/46eb8567302eaf787abf88d09df014e14ae3baf460af1b8b0efdbd3efcd5/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476", size = 676634, upload-time = "2026-08-10T13:40:53.004Z" }, - { url = "https://files.pythonhosted.org/packages/4f/18/8d58ba1c429b0383e3219a3d0e0bba241d0444d8ed05b73349953c7d7c7b/greenlet-3.5.5-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:102817506f6090b5176c746a82603341a549b40e5c3d5b72a4c672228a918c41", size = 510175, upload-time = "2026-08-10T14:30:07.047Z" }, - { url = "https://files.pythonhosted.org/packages/a3/e9/b88bbf5b29970cb84172dc2c32aa3e5e579ceb94c808e81c826454138850/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874", size = 1637320, upload-time = "2026-08-10T14:15:09.317Z" }, - { url = "https://files.pythonhosted.org/packages/6d/8c/7631ed29cc6f0392f11830076e172ce4885e70b0bc2c1bce1731176d4b4e/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71", size = 1697412, upload-time = "2026-08-10T13:40:34.924Z" }, - { url = "https://files.pythonhosted.org/packages/da/0f/f7dd935f9c4cb1be49098770587f54d8a78518e55c89bce86c4fb4109057/greenlet-3.5.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0", size = 331514, upload-time = "2026-08-10T13:29:20.611Z" }, +version = "3.5.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/6e/0091f175ccd02b02bc8811bbcbcc6ac2e980be116e3b2f7a736ca322bf84/greenlet-3.5.6.tar.gz", hash = "sha256:8e67c43bdfc88d5fee6db0d3e40175b362fc95fb85f0412d233b9b203c53a575", size = 207653, upload-time = "2026-09-14T15:42:51.806Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/18/3fc6d951466ae9a2a688edcddde3b2e388da0a8244e0caf7117bbeb0eb95/greenlet-3.5.6-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:a5876d0a60355af98d535c47f6cd6eb0f8a432396dab26845d380b92f8412422", size = 295668, upload-time = "2026-09-14T14:22:33.241Z" }, + { url = "https://files.pythonhosted.org/packages/27/89/366d2af5061eeefa5012f510d95a99c8620dcc457609838db4d538820318/greenlet-3.5.6-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e85880b538e59a59f55117b81f208a6660ad5ac328aad9305f812d9b8bc67a0f", size = 611700, upload-time = "2026-09-14T15:12:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/54/1c/07f133f865fd58ae593dd2bbec3144acaee9b04ffe2eb48c6e121747ceef/greenlet-3.5.6-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f0ba7c2a329d650628f4c8572fd1db29f0a59dd70a3e3e0710dcf18a35cce9d8", size = 624223, upload-time = "2026-09-14T15:20:42.459Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f2/844dc823ff2752ad049caa6b59d57e4572f9c445934b02d3518f4c67197c/greenlet-3.5.6-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ee7d9da3bf493909cf811a3f038840cb34fab5ae2956b8a263919f6e289ab188", size = 629529, upload-time = "2026-09-14T15:25:06.354Z" }, + { url = "https://files.pythonhosted.org/packages/66/6a/1594f3869c57c149abdb380492529e04d4c0229b5e4d79572c5bd0aaa673/greenlet-3.5.6-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:975736b002ed080d124cf81a79cb7e05cb26d6b3f5c7a7b651c0fcce70353aa1", size = 621404, upload-time = "2026-09-14T14:35:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/c0/42/b1f8dbc89a53b9e77859fc1ad1627d106fc361daa3ea4bdf43a91ebb4338/greenlet-3.5.6-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:71890d5247020c25c21a6b65202782bfc281d4e6e244842419d30e3492bb6dcc", size = 432385, upload-time = "2026-09-14T15:28:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f5/33e5c9e48178b9259fd000f8f45caa4a65036f65d3d0c06a602f570f025d/greenlet-3.5.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0616b8f878098c5681fd8f0dc92d887551717402342a70f0abcbfea5f5ad8a44", size = 1584998, upload-time = "2026-09-14T15:10:06.653Z" }, + { url = "https://files.pythonhosted.org/packages/ef/31/9b4e140bc24d0ad7927ebd651f5608b0acc2334d061748c3b6ad19085cfa/greenlet-3.5.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3dbb4596a6a4e5d47121a33ff20533a81e60f302d9e67b69909a8bc21a43f0a7", size = 1647568, upload-time = "2026-09-14T14:35:49.787Z" }, + { url = "https://files.pythonhosted.org/packages/c3/71/d79f1791f824f8ff15c2978746640467ae932a2365e0201069f7f272395f/greenlet-3.5.6-cp312-cp312-win_amd64.whl", hash = "sha256:7ac4abb3877c43af320392c664774eef6fa2cc063c79a55fc02d844a3cbe7395", size = 324203, upload-time = "2026-09-14T14:22:54.504Z" }, + { url = "https://files.pythonhosted.org/packages/63/af/42aca4d56e8cb321912203069d8d34734cb288222f10ad2ae102718cc577/greenlet-3.5.6-cp312-cp312-win_arm64.whl", hash = "sha256:301102a49120b095e72a7838792b41233975fc1c155daec6d98f81c00c9280e0", size = 308310, upload-time = "2026-09-14T14:24:03.008Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a1/e720a38852366c589e1a46cf570b886507ad2cf591050c203365638baab0/greenlet-3.5.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:f96f0e30b5a95c7631b12bfe214cbc90ec8fe8cfa36920596c10514a65743519", size = 294627, upload-time = "2026-09-14T14:24:40.102Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c3/58187858df41354a11e6a55b421e7af9059798abdab3a384cc51b8567c38/greenlet-3.5.6-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c75116c9de79949de23006e2d9b35ee82874c594fcf5c0311b439acaa14b8441", size = 614356, upload-time = "2026-09-14T15:12:03.399Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b9/3a7e67d5f05c9760b1ad411fa52264bd69cc08e22a2ebfb4018b90628ced/greenlet-3.5.6-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cad5782f93f7f738b62c6527b6f32a60694d924029f299a8b524758cfa53d815", size = 626756, upload-time = "2026-09-14T15:20:44.269Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7c/40400455f5b5a65bb83e94fde66d1be9e5ec518638113f8083ace746c309/greenlet-3.5.6-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a93ee7c6e8fd0f8a83525a51bd777be57ee17787e91d805bd8d6faf9dcada18e", size = 632632, upload-time = "2026-09-14T15:25:07.813Z" }, + { url = "https://files.pythonhosted.org/packages/85/cb/ab0c123c514ed4e94c0dc9ee2e86362633e6b998cfc05de7fc9ac2eb9690/greenlet-3.5.6-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f98e8215e172f567ce80eeaed9107fb4d32b6c44f26983d9b8334658136a205a", size = 623779, upload-time = "2026-09-14T14:36:01.104Z" }, + { url = "https://files.pythonhosted.org/packages/f9/67/1f35cff30a6c51c3f23b63d4afcc7313ab4f97490ba3676fa78178984b27/greenlet-3.5.6-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:7f731ebac68ea06d628658295cb2d217b10186329fcf9a3b6a149045059bf92e", size = 434933, upload-time = "2026-09-14T15:28:38.858Z" }, + { url = "https://files.pythonhosted.org/packages/a5/26/fda8a5a06e7073333ccb038133c5893b9e0c4fe29d5992a17e83c241bc6e/greenlet-3.5.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df19e2d0b1620039af5102563fbd96e8938c7f5c3f5828528d641d9fc585525e", size = 1584930, upload-time = "2026-09-14T15:10:08.234Z" }, + { url = "https://files.pythonhosted.org/packages/2f/37/50f8813163148d6234e08b23dcad6a9e37f01d148c8ec976e4c44ea2d918/greenlet-3.5.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:06c0e933290fba8ffe53ead4ae1b8044b0e9754b75cebf381aa2bc3e50d82fac", size = 1647590, upload-time = "2026-09-14T14:35:51.173Z" }, + { url = "https://files.pythonhosted.org/packages/86/da/b7669b09586365654083a62bd0724cf06cb74bd5085a15cdd161271f992f/greenlet-3.5.6-cp313-cp313-win_amd64.whl", hash = "sha256:5b602b4201b965a8354d74e232364a66ff243dd142e350d035f46169bb36e13d", size = 324086, upload-time = "2026-09-14T14:23:48.428Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5d/c9663cfe84a2a9e0aa96f066f5b0594c227ea4c647511e087e2e11d4ac0a/greenlet-3.5.6-cp313-cp313-win_arm64.whl", hash = "sha256:876077e7ebb8c84ed068e2b23d4c62ebb010d60df84b9591af1be2f39010ffb2", size = 308211, upload-time = "2026-09-14T14:28:01.634Z" }, + { url = "https://files.pythonhosted.org/packages/66/c0/d254544ae2b8bdd311aef000fafc02828c2771b17d994b3075620ea7cc6e/greenlet-3.5.6-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8cddea1b8339451c2fb3388e138347b6126744f33b611bdb55b7357361cfef46", size = 295221, upload-time = "2026-09-14T14:25:11.583Z" }, + { url = "https://files.pythonhosted.org/packages/18/18/eb54be16b9cc3971e09ca5b73334e1b8c804a4630d9addaaf218a4fe300f/greenlet-3.5.6-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c59acfa8eb73a1e0d484392dc002bdf001fd4ce73394e0132df3d1ab6093d7cb", size = 660992, upload-time = "2026-09-14T15:12:04.876Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b4/e193efe65671dcf294bc51fcc59efb52d154adf8612c4ea016da0d2c486c/greenlet-3.5.6-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3b4a01c6da07ef9f80d4fe8933b994bc99747bcea3eab0330a9c34d3c12655b", size = 673428, upload-time = "2026-09-14T15:20:45.756Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/631bb45fafde1dca782152377c0676d182ec924820064047f533a3627b28/greenlet-3.5.6-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dd0b83bed3405b586a3133629f1d1a5bc7bfd64822a3b7ab342bdc68e6dbc61b", size = 677688, upload-time = "2026-09-14T15:25:09.279Z" }, + { url = "https://files.pythonhosted.org/packages/45/ac/28fa7a9e50f2859466214c4ac584d776db52c1604ad4dd158960a5af2a1f/greenlet-3.5.6-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a09d59bef1db94f384b5bcc2d523694d338f3df6b757aeeaf7baca5d0c0be88", size = 670773, upload-time = "2026-09-14T14:36:02.577Z" }, + { url = "https://files.pythonhosted.org/packages/40/30/2b0a73e68e1e18e30b601d0d183cfdfc2beca4de5a6843c630f0fc9fb90c/greenlet-3.5.6-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:fdacf26402389bdd89857ad3c045a26fe8f3314f9a8b28226f82f88463a65b77", size = 480475, upload-time = "2026-09-14T15:28:40.741Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cd/fb7d6cdd86ff3427c1494854f0e35437eba05142be91f530f6da75e09e19/greenlet-3.5.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8b7c73d1cef3d9ae963e9ff03f6222df43efbb9054ffd2f1969c935b7fc84c02", size = 1631900, upload-time = "2026-09-14T15:10:09.745Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/143bdbb20a516628cb15074ae52ed17d850b450292609c7a6fccac6dbece/greenlet-3.5.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8b27df301f56e3b3d2298095c8f7d6b68f2521f6b1693e901fa039bdbae34424", size = 1693740, upload-time = "2026-09-14T14:35:52.959Z" }, + { url = "https://files.pythonhosted.org/packages/c9/9e/019642432e6ae283301df1361227d47610709d2dc69a38f95edef266d713/greenlet-3.5.6-cp314-cp314-win_amd64.whl", hash = "sha256:f8f0bd690e1a41294ac87905e8121c81a3761ec2583c768f13467428606c8c7a", size = 327473, upload-time = "2026-09-14T14:28:12.948Z" }, + { url = "https://files.pythonhosted.org/packages/e9/7f/8aafc7bf70c948786dba7221d0dc0838e5329bebc6d434ef2208b4f0e760/greenlet-3.5.6-cp314-cp314-win_arm64.whl", hash = "sha256:8cda13494d86a4f12429641117cb6ac4bbbc9c30a33f711f7d3a2e5fbe4b0b7e", size = 311095, upload-time = "2026-09-14T14:28:00.7Z" }, + { url = "https://files.pythonhosted.org/packages/14/7e/7a205688a5b3074933b18a906608d46d106e9a79d776bdab5a4abf4b4feb/greenlet-3.5.6-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:97c5a53e8c1754df58e73f047a99e287d4da1bdfe64b0072fb25c87000897951", size = 305352, upload-time = "2026-09-14T14:21:31.962Z" }, + { url = "https://files.pythonhosted.org/packages/78/cb/9c4a57a9d9dd0256e20b8f7f4f06554c2c92badebf0ab73ce344321b78b9/greenlet-3.5.6-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fea4427d1ffdb3b523d7daa6712038428a4c16c450b9777bdd1221cfee0eab49", size = 672671, upload-time = "2026-09-14T15:12:06.347Z" }, + { url = "https://files.pythonhosted.org/packages/97/52/c6729681ebbd298f4decd28746815acc8a0b0a0fde21d2df33776fd4d042/greenlet-3.5.6-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73a29b5ba642e35433166a03a3e02935e7238c4b3467fbd77523b99edea23e5b", size = 679489, upload-time = "2026-09-14T15:20:47.291Z" }, + { url = "https://files.pythonhosted.org/packages/71/76/3c11c21e0716b1f1dc7c1a4b3d690abb1d3b448c69a9d32049fecb64010a/greenlet-3.5.6-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:61a61b4a95a4f97922c3a6f5606d3e360851584bd47e500a5161373c53810e3d", size = 681303, upload-time = "2026-09-14T15:25:11.088Z" }, + { url = "https://files.pythonhosted.org/packages/58/c5/2b6c721ba8b8963da42d5a0f57f25b8aaeb1fe9bdd156875e57f3be648a2/greenlet-3.5.6-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:460e70b033aba8ed47e2ac9b5d0d2157b05a34fbfa30a241400aef4118902cdc", size = 676608, upload-time = "2026-09-14T14:36:03.959Z" }, + { url = "https://files.pythonhosted.org/packages/3f/26/3ae402202452cd5941bbbd483e5a74297e2397e7aa3182c2a5e3ab7d5666/greenlet-3.5.6-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:fe3170a69fe039b18ad18171e66faa9a75f6fe9d78f968fd9b54e09fbd714d81", size = 510112, upload-time = "2026-09-14T15:28:42.112Z" }, + { url = "https://files.pythonhosted.org/packages/b2/04/0d018e0d05bcdde19a0fcb907834155f1fc853a9bedd3f3f5e6acadcae19/greenlet-3.5.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca80a49b53ed1d22f7282da7255f7bb2fd1935fd0f623d8613fda38745f18961", size = 1641479, upload-time = "2026-09-14T15:10:11.216Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/f02ef9073919158f6403fe3701d4ed4403d646720e7201dfc6e9d264bac3/greenlet-3.5.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:916f92f2a8db10508f739d0b5e00b83defe5d1115a997c54532a6d7cf8c95404", size = 1698758, upload-time = "2026-09-14T14:35:54.336Z" }, + { url = "https://files.pythonhosted.org/packages/08/a5/1f48fe647473a2dcccfd1839b2ff2c78eb57009be776b4da071e901c9bff/greenlet-3.5.6-cp314-cp314t-win_amd64.whl", hash = "sha256:886bcf1870af74c32bc310fd00a6b803445e17e51b7d5a107c7b35c0f362cc16", size = 331574, upload-time = "2026-09-14T14:27:18.451Z" }, ] [[package]] @@ -1626,57 +1626,57 @@ wheels = [ [[package]] name = "grpcio" -version = "1.83.1" +version = "1.84.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e2/b1/46539f5050d7c316a13396d185451f95084a74ddc68b12d818595bef0377/grpcio-1.83.1.tar.gz", hash = "sha256:9cee6fcbf2eb57c4b49451787bfa87be8efc1ca02a0b327dd4b54d44502e362b", size = 13445033, upload-time = "2026-08-28T07:09:11.464Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/85/9e/a3ba13e08bbee5bf6e57597dfe4823961fd7e94c0b8afe3a4bb7dca639f3/grpcio-1.83.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:5acd14c6ddf047de62cbf8745b11103ea91abbf57d1b8edd5395ccd9fcd13abb", size = 6303170, upload-time = "2026-08-28T07:08:08.188Z" }, - { url = "https://files.pythonhosted.org/packages/1c/ae/65ce56a2527faa17d02cba4c2231c74047ad898be339486ba87f093bfb66/grpcio-1.83.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:16138031a47b771860a16a975b53087f4fd5bbdbb2c03a188c5d90ad65d2bdae", size = 12165806, upload-time = "2026-08-28T07:08:10.309Z" }, - { url = "https://files.pythonhosted.org/packages/4e/91/40432480088a2243d360864de072ed5b78c4ebbaabd29c28918f1e1b1454/grpcio-1.83.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5ccc26715fd4defca5e129e280dd883b1737b65045ec50ffe22ce42104089519", size = 6872490, upload-time = "2026-08-28T07:08:12.355Z" }, - { url = "https://files.pythonhosted.org/packages/c8/62/3da2300c8c79fd20a78a8a4bb6251e5068d9af33bc8fd389b98fec35e8a3/grpcio-1.83.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b74f2a1d9ab1dfa3e263ef33d581613679b78d0884babf11671af26e45570ead", size = 7618367, upload-time = "2026-08-28T07:08:14.025Z" }, - { url = "https://files.pythonhosted.org/packages/bc/19/9fc702e31a631262d7a752fa699f6022821e707fefc8bff49b1550a57729/grpcio-1.83.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72578aa07a4008f17521ef52debcc3acfd1e2c5426243bc3ffb56a38bfe610b7", size = 7040936, upload-time = "2026-08-28T07:08:15.963Z" }, - { url = "https://files.pythonhosted.org/packages/ec/56/95933cc44cba2429765fa065c951dd529e5771b119d9d2481b4646f1d6a5/grpcio-1.83.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c12e1fc59c6dc26d10d9144453ddc6cbfe4cd4c31e874ed2d0132f88e685eb8b", size = 7573096, upload-time = "2026-08-28T07:08:17.729Z" }, - { url = "https://files.pythonhosted.org/packages/ac/80/af63359da06b016de48cb111f144703a10043850dafa43ae0a038907b9e8/grpcio-1.83.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4910b62f7d12197160bfb7de06d876d64dd12d43483e8292f98f49ca09b628d9", size = 8609442, upload-time = "2026-08-28T07:08:19.777Z" }, - { url = "https://files.pythonhosted.org/packages/3d/fa/f0586c56bdfb8a7a2adda01e0ac2413447cde3141ab09411a5d5afdcffd3/grpcio-1.83.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9e703effe3ae779925c82ac24fdb82cf4105e1096810151ed9501c5f34546b9c", size = 7984321, upload-time = "2026-08-28T07:08:22.114Z" }, - { url = "https://files.pythonhosted.org/packages/25/8a/14ec05669f9eb295801e26c2ea8c561a1b786b0e3557c2c22131165ab010/grpcio-1.83.1-cp312-cp312-win32.whl", hash = "sha256:a2aea8bd6e0a34f12cbaddb7bb70bec836818789fa5c7ab7572c6b745396a2d4", size = 4395604, upload-time = "2026-08-28T07:08:24.08Z" }, - { url = "https://files.pythonhosted.org/packages/e9/37/8c2f7cc16089e36a3fbacaacc7a3d043912aa0d2dfae5556f6450414ea6e/grpcio-1.83.1-cp312-cp312-win_amd64.whl", hash = "sha256:583bf2e8255040a4a312f9572dfe62a05271437b149550e1a536d5c47d2d1e8a", size = 5161512, upload-time = "2026-08-28T07:08:25.81Z" }, - { url = "https://files.pythonhosted.org/packages/7c/fd/d1fc58933bf88c9209f89dc570c810f1aa57cb04b3459cf2b26f61e32112/grpcio-1.83.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:8d228e253b77865efcbdd7b5894ca882c9e0ea98c02b7d20582e61ded8dfd4b5", size = 6305628, upload-time = "2026-08-28T07:08:27.872Z" }, - { url = "https://files.pythonhosted.org/packages/c4/49/0b40bae059c619505c9b751cee6caa208e4904e290aaefa1728c4c2c67a5/grpcio-1.83.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:0468b627f2987c9a77f7580030207cbd85457ffe52998beff4f0b5c38c58a72c", size = 12156839, upload-time = "2026-08-28T07:08:30.191Z" }, - { url = "https://files.pythonhosted.org/packages/61/4b/e8c0d635da0ee5ddd9950c8d540f5dcdd0ef1854a382cc55496a487a8d31/grpcio-1.83.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a6a282e81530cead60bbd752cc04950a57f224379e9821495d6a35bd5ce9b1f4", size = 6877036, upload-time = "2026-08-28T07:08:32.285Z" }, - { url = "https://files.pythonhosted.org/packages/c5/d4/760a33f339a7dd3d5f4b3e0e9bec5472d95592a80f887b2e9dab4e41cfbc/grpcio-1.83.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:947d945f52e8ecf3cafd2bb7113502a16ccfda3e12c854443094de32d83ad432", size = 7624404, upload-time = "2026-08-28T07:08:34.194Z" }, - { url = "https://files.pythonhosted.org/packages/54/ec/bd798654b06fb42a92b57d1dc1b530084fa89ed442806fcd0a833a36f9b3/grpcio-1.83.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:55656318d5dd387077396dffb929171ca3966e24bfead9a6c5dba9f889062cb4", size = 7042942, upload-time = "2026-08-28T07:08:36.208Z" }, - { url = "https://files.pythonhosted.org/packages/08/b0/c00f86614566dd0961825cf0f43d4f96a74371d9d95f952bcbc4b86d9a27/grpcio-1.83.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9daf5acf4fc9d5f5627229969c2580a91e511779d76e4ccdeb9f4770f05d8bc2", size = 7576937, upload-time = "2026-08-28T07:08:38.041Z" }, - { url = "https://files.pythonhosted.org/packages/b1/38/85eff43a5c89dc666a252b5c9f8e9ab03f89e11c95b6263d2933f08fdbe7/grpcio-1.83.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7b94174cbca93316888f805efbeb08f1c020f7b7493d2d50cc4f6b64ebb7e8bd", size = 8608391, upload-time = "2026-08-28T07:08:40.092Z" }, - { url = "https://files.pythonhosted.org/packages/35/4e/82835483e2f812494be865e7965c0d626cb9e71ab0d83a420d75aea4ad67/grpcio-1.83.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:65c5a7210911ffe0f67b1cdc5308f9854b6d1f1b345e3e49ab7cac1ba50fa346", size = 7980060, upload-time = "2026-08-28T07:08:42.434Z" }, - { url = "https://files.pythonhosted.org/packages/b2/b7/68a98bef733fef704fbcfb3957c8dba67e3e38ca7a7fea851195bc97c648/grpcio-1.83.1-cp313-cp313-win32.whl", hash = "sha256:179368d9361854616ce6f397d4716e07480129652752fcbcfc5a7260455ad6f2", size = 4395226, upload-time = "2026-08-28T07:08:44.463Z" }, - { url = "https://files.pythonhosted.org/packages/85/a0/df4de3b51d37ac8fb0320bb9668381ce2bd3b7aa990880bfc56a8a26f665/grpcio-1.83.1-cp313-cp313-win_amd64.whl", hash = "sha256:2e57af456385491a76e13c4aada8c8f43a8e47051e06ea97a9dbe2a49654e6db", size = 5160273, upload-time = "2026-08-28T07:08:46.216Z" }, - { url = "https://files.pythonhosted.org/packages/42/9c/484d981d8b90c4e6abf3030bd2ed747e84d1eb192b3ec9cbb41e0b73e4bf/grpcio-1.83.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:8b3c87ca908296bf125f841d3e1a2225a2b39aaa8ed7a57e7ccde465ee519bab", size = 6306089, upload-time = "2026-08-28T07:08:48.379Z" }, - { url = "https://files.pythonhosted.org/packages/84/01/0afec1c92e4f292f74a44ecf75eabbf40903125b8c4df103c9868d6338da/grpcio-1.83.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c0f3f20c90e72a171917ae65706500b096a1c3eb5f162c3ce702a2e25635f132", size = 12170381, upload-time = "2026-08-28T07:08:50.653Z" }, - { url = "https://files.pythonhosted.org/packages/c7/5a/e9a2383804433a0a61d6d93777ad321c7f36ac1cfdaa4c6d1a7c9ac846b7/grpcio-1.83.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81bbf35a46bf8cad2dfbb2eccc19c711befb58b288acb534bbcd0d74283202a6", size = 6883286, upload-time = "2026-08-28T07:08:53.654Z" }, - { url = "https://files.pythonhosted.org/packages/63/e7/f8ca8f76994e14c70b9a0052e82f10de497a23db450c36379c9716ebfc4d/grpcio-1.83.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:215cec07d11176507387bda4bf2751816e880f9bff8dc1ca524bfbb8ed8f2fad", size = 7624293, upload-time = "2026-08-28T07:08:55.709Z" }, - { url = "https://files.pythonhosted.org/packages/bb/7a/4b672814b0cd0fe63bdd735379d88b165759f3144ab023ad8ec5fc4d53ac/grpcio-1.83.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:abce7d43ec29cd39230fa8339de1a07643b55adc412a454850fbd875349950ff", size = 7044346, upload-time = "2026-08-28T07:08:57.802Z" }, - { url = "https://files.pythonhosted.org/packages/50/b8/d89fe60e4239ad51be333dd9cc703741d449a35064e51f8a0b5bfa755432/grpcio-1.83.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e256f95a40e3b0183a98556fb7164d24b97eeb353123ccabfcba94712b35ee2a", size = 7584187, upload-time = "2026-08-28T07:08:59.867Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b2/b290d7402633d9166e4dd47e6f5f74a24ce10a8340b84455896ebc349f85/grpcio-1.83.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2110059146fb0ea216e1ffddb29377b5cc2fd412a5b0a92e102616bd5edf18c2", size = 8608730, upload-time = "2026-08-28T07:09:02.592Z" }, - { url = "https://files.pythonhosted.org/packages/f5/44/fa89e44d1b5cf5b9fa71b2fd7abf506f182fd43917231a92fbf1ea326b02/grpcio-1.83.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20d944d967843f8183f9f23d5916388362e5f8eeeae855bbe4354d906dc9f31b", size = 7983283, upload-time = "2026-08-28T07:09:05.087Z" }, - { url = "https://files.pythonhosted.org/packages/ce/b8/9db73ed1f35ffa76124ac574bf296d06a359798dfd6b50d382f2b8a060a1/grpcio-1.83.1-cp314-cp314-win32.whl", hash = "sha256:623c87c6d4a1cb30d82c4e896f95477050f2e01b4a1f8cf91ff2b1abdf89c457", size = 4474327, upload-time = "2026-08-28T07:09:07.179Z" }, - { url = "https://files.pythonhosted.org/packages/65/22/fc9a622d885a7a37ff972a12faaef443d74e47407181da70d0ab62ab41f0/grpcio-1.83.1-cp314-cp314-win_amd64.whl", hash = "sha256:47e6934ad38779271e2e7cc5f78a63a407cf3d98114c65c1fdbcd3f5a716f29b", size = 5302032, upload-time = "2026-08-28T07:09:09.285Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/3f/4f/4435c0aae54657258d9cfcba78598f3d9e5fe4c82ff18d78558567b90faf/grpcio-1.84.0.tar.gz", hash = "sha256:19aaf172fc2edbefccce3f6e92c5150975dbe56c45744e9e87cf72ebdf85bfbe", size = 13493876, upload-time = "2026-09-14T06:59:33.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/c1/4c9a2e0e6b0aaf02781404cad2f79211f989f2c827cf672a4a48d1604d3e/grpcio-1.84.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:b5c6f20d657ae09ae4e30d9d3a21edd13f1219d58cc6f999b9d1bb63be9c1baa", size = 6415756, upload-time = "2026-09-14T06:57:39.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/57/131e7007bdee9acb77a8dbe8a16fa9fef75f88c1695242d8ee0993ac2d3d/grpcio-1.84.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:406583b4e8fb2282ebd392e12b963e601c1f82e07125a8c2cb5b144e7e024796", size = 12339195, upload-time = "2026-09-14T06:57:42.373Z" }, + { url = "https://files.pythonhosted.org/packages/db/d1/a7b7cda98fcab9b3d2916204a872d87371158a7a34e41768f524584fb64d/grpcio-1.84.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fbdbcd06986ede3ce584083b1dc2afe6808e8943e5cf50ad11183c03aceda25a", size = 6984468, upload-time = "2026-09-14T06:57:45.035Z" }, + { url = "https://files.pythonhosted.org/packages/19/81/c5be83e3ac9416f73c4c51fe1ea9c41a0c42fc3509e3505faa46f5046abe/grpcio-1.84.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:23e6e8e8a75cff88e0a793bfd3becea03a13e2763ae90c1ff573bc19ca5b429a", size = 7749432, upload-time = "2026-09-14T06:57:47.395Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bf/258cd7c0a7ed92745dc93c31666d462d05b702807a689744bd49fb833bde/grpcio-1.84.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b44f0a0fc7bc6677d38cc80bca1a32814ce6c8f200fb8b3c1a61c9d77eaefbf3", size = 7156115, upload-time = "2026-09-14T06:57:49.657Z" }, + { url = "https://files.pythonhosted.org/packages/2b/4b/7f829418dbfcf91b875e55e2973f1059a95decb4f081313416317ef04ec1/grpcio-1.84.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:210e4c32f907045eb8158273e60c6ab69a3947697df6245dbda381f26c59485b", size = 7708010, upload-time = "2026-09-14T06:57:52.496Z" }, + { url = "https://files.pythonhosted.org/packages/34/f0/9932e2fec6a04205f8bf3f8f4d2020479dcdac88feb6f93822ed31bf0eba/grpcio-1.84.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a71d24f40b0cc6798feaa978c7411dc1135b7018e9fc0442db611c139bf58344", size = 8759980, upload-time = "2026-09-14T06:57:55.312Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5c/b67407c6dbc480dfc0715f6eccdb1061e7c88d85f9a330a241d357a538c5/grpcio-1.84.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f6c972474ce691aca74e58d17625450cef153dc4760364cadeb167983ea6d589", size = 8124904, upload-time = "2026-09-14T06:57:58.569Z" }, + { url = "https://files.pythonhosted.org/packages/02/37/2bfdae2df8dfcfc0df619b628e0c7153ce703adae827243f44720322ccc1/grpcio-1.84.0-cp312-cp312-win32.whl", hash = "sha256:0d532ade4486dad9b302ffa4d4683d67561051c26d17c4023322845e9fa10140", size = 4478915, upload-time = "2026-09-14T06:58:00.714Z" }, + { url = "https://files.pythonhosted.org/packages/85/2c/309268b7b39f6deb2342f634841e105623a0b67982e8b10ec516782ff1c6/grpcio-1.84.0-cp312-cp312-win_amd64.whl", hash = "sha256:49717e857899f4136d7657bf5aded61ac479110a075438290923a4d86af7cd02", size = 5253534, upload-time = "2026-09-14T06:58:03.336Z" }, + { url = "https://files.pythonhosted.org/packages/5d/51/40f99701adb01d4e5316a2aaf13838da1a24d5c879cd8c95156d7c364454/grpcio-1.84.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:209414080da8c20af94df1395b635da52dd57b5edc9e917e1deca0dc1c4bb55e", size = 6427619, upload-time = "2026-09-14T06:58:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/c5/4b/ed8e22a1237e6b2be6ef4f221d074a5b0e0dd8a0da8c944c04aea731f0eb/grpcio-1.84.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:e41c3993eee896c617dbd8a505085d28b6e84a0445ed9a1f40f95808473cf678", size = 12336549, upload-time = "2026-09-14T06:58:08.583Z" }, + { url = "https://files.pythonhosted.org/packages/d3/50/00165b05cd73f45996748ea67ce9e55d08936f2fea94a7fd8541cc2d0e54/grpcio-1.84.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fff5ef3fe1bba7d6147e5f19e01e5e122ac2c076486887ddcb8d42e663400fbe", size = 6989458, upload-time = "2026-09-14T06:58:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/26/38/d0486230e684d916f97429a53041db88410e662a38f2a8d09e2d90375840/grpcio-1.84.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b8c62888c3e49debf37ad9773e3c02f77b0c1e811f8fb0962f2b6c3bbab5b97a", size = 7757778, upload-time = "2026-09-14T06:58:14.849Z" }, + { url = "https://files.pythonhosted.org/packages/da/56/548a643decb059ca244499c675ae2c13a15f523ba94592c2774bd80a13c1/grpcio-1.84.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:986e9751d416d7a6eaa2fecdac38da63153d63a4b340ba7d624889c490451500", size = 7159572, upload-time = "2026-09-14T06:58:17.87Z" }, + { url = "https://files.pythonhosted.org/packages/db/f5/42caac81a79ec680f1f7a8eaf7ca90d2f93936ce0c3a073141ba96757f77/grpcio-1.84.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5933a052946873d01a42119a05420d669bdca436aeba2d1851988ccb12b421c0", size = 7710547, upload-time = "2026-09-14T06:58:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/57/a4/828ad990b2410fee0a55cc73aa1bf98eb5b911c54847374ef4f24b9e877b/grpcio-1.84.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e094dd21f077af8194923fc263cad872eaa1802bb0156fd7e5ae18e99cd86715", size = 8761519, upload-time = "2026-09-14T06:58:23.875Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a5/1f91af098919eaf5d80d5a61126ad9fae074e5190c25a3014ce1d8d0d890/grpcio-1.84.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08735e3d08d24ab3132cf87e2e5dea8746cabcc7d676c2b0b7362f195feef9d9", size = 8121424, upload-time = "2026-09-14T06:58:27.006Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8f/77fd4a7a913b636785479922349c4cb98d94d05d15652e556b3ca0df6663/grpcio-1.84.0-cp313-cp313-win32.whl", hash = "sha256:70bb4ce8be0c5606bec259cbd7152374470396413b7863a658a08c849e6b29ff", size = 4477974, upload-time = "2026-09-14T06:58:29.528Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9a/1fa59ddbfc8898e5518d1447e46f771f387f0ed6132ad531395338e51a5c/grpcio-1.84.0-cp313-cp313-win_amd64.whl", hash = "sha256:b61692f0069b3eee2fc8a3a1b7f6c044df9e03fede6ce69b3ca832e1c39f26c5", size = 5255326, upload-time = "2026-09-14T06:58:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/26/6f/e25ca89ca5b0b7b95464c907a5c21a77c0ac8c4ee1dca164c4dd8f153ddb/grpcio-1.84.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:026d757df86c5b7a41de8200b9a2cda454aaa5004cb0c7e3374c66eb82f61499", size = 6428207, upload-time = "2026-09-14T06:58:34.401Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b4/6b76b429f3f9b901cdbc306c81364d708bc957f847a05cbd1046cd2d05d8/grpcio-1.84.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3de427b05f244ba2c2a9bdc67e7a6731c8340811524ecc4435466549f8af1d17", size = 12342420, upload-time = "2026-09-14T06:58:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/af/64/ac86d638ba7f73bee0dccb608ba551d4f63adf75151f00d2c43e46d3979e/grpcio-1.84.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e90e3bdf7b5eac005fef631adae9cafde16f922def207b80a7c46b253c18ad20", size = 6998396, upload-time = "2026-09-14T06:58:40.535Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/fa12e9ec9d7ebf8cc3e81428fa9e1ca0d30d22d546ce2baa4c64bc917cbc/grpcio-1.84.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e88d304f094f4937bc27ec6a435e218a084168f11ec630c8d5d39b431d08d81d", size = 7757538, upload-time = "2026-09-14T06:58:43.297Z" }, + { url = "https://files.pythonhosted.org/packages/21/d7/94240c7fae121ff1f116dcf04a3b7ee0216a06832c704310363f72638d4c/grpcio-1.84.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:57dc36a5ab0e676f5f6e171de2917fd0aef73f32a9aaf23956bfe19997a30bd1", size = 7161480, upload-time = "2026-09-14T06:58:45.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/c9/7033e95d4b344969818b09185721c7608b47fc2498d97b5e4eec4995dbf3/grpcio-1.84.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5deda5b4bf62769eb98c119cca43d40e1231e34846b19db5cdea821d446a2253", size = 7720191, upload-time = "2026-09-14T06:58:48.308Z" }, + { url = "https://files.pythonhosted.org/packages/95/22/b45df2deba81d55069076859480bae7109c9eec02bce5515c799530cc2aa/grpcio-1.84.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9bab4cf571653a8afffb83ce21aa27b51dfe629b526b7b6adec35491fe1fc2ea", size = 8762792, upload-time = "2026-09-14T06:58:51.068Z" }, + { url = "https://files.pythonhosted.org/packages/de/c4/3e1c3d6155c16b8737cc31d5b477d6cf1fc7cdd10d58320cf0ec9b446f42/grpcio-1.84.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c5559b492007dc09b4de9b95dab05f0b5e53547aad230cf07e46c7dd017a3be5", size = 8123299, upload-time = "2026-09-14T06:58:54.332Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/f4864de5b815e5ba18858771f99381a398fac14117f89ef5291ed43d3c4e/grpcio-1.84.0-cp314-cp314-win32.whl", hash = "sha256:2c024da73b296f040b8360e60bd73a659b230093684a438da0e1260f34cc724e", size = 4562560, upload-time = "2026-09-14T06:58:56.894Z" }, + { url = "https://files.pythonhosted.org/packages/44/03/640811d4d8c84f5e603995c5a9bab725223aa472cad9ca4286c3bbf1c3e3/grpcio-1.84.0-cp314-cp314-win_amd64.whl", hash = "sha256:800b7e00d92553313c0463c200087930aa78678ec1d528193aeb50906f55989b", size = 5394092, upload-time = "2026-09-14T06:58:59.61Z" }, ] [[package]] name = "grpcio-status" -version = "1.83.1" +version = "1.84.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, { name = "grpcio" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/03/dee41fe15a9657c60397c1f215622a1f146e174367bbbb67911b62ee0629/grpcio_status-1.83.1.tar.gz", hash = "sha256:c08c8d553d6ab96effae1d923839de22926f5a316a942f48a48040e047c517af", size = 13957, upload-time = "2026-08-28T07:12:25.893Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/45/f80309cdb6a7dbf8f65e2082dd2ddc9797ba7180516a73c54d966ba632c4/grpcio_status-1.84.0.tar.gz", hash = "sha256:5caf28ba7184b81f618b5f7f094859fd2541bf429d2189bbbcd715c9c2cdcee2", size = 14015, upload-time = "2026-09-14T07:10:29.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/34/b03ec688e5c8c6ce283ec8b214d9c171e97059eb55157465f6fd6db1562c/grpcio_status-1.83.1-py3-none-any.whl", hash = "sha256:2cd328ee62ef2b3eb957dd3b75db7dadcdbb76488fdc9ab3aba1ebfbbdc324a4", size = 14636, upload-time = "2026-08-28T07:12:15.982Z" }, + { url = "https://files.pythonhosted.org/packages/71/c4/3a77e4273e866b1b0c412afd80882e95941a37170032b5109d847c501124/grpcio_status-1.84.0-py3-none-any.whl", hash = "sha256:0c182ca0d6e60acbfd0e14499cf39a155e4827a1c3fd9f7638e49af15a74c30a", size = 14639, upload-time = "2026-09-14T07:10:15.175Z" }, ] [[package]] @@ -1727,15 +1727,15 @@ wheels = [ [[package]] name = "httpcore2" -version = "2.12.0" +version = "2.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "h11", marker = "sys_platform != 'emscripten'" }, { name = "truststore", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/8c/e925b1c92018abb3a1863ce1549d76d2381e334d21d65d4ac8f65dabd78a/httpcore2-2.13.0.tar.gz", hash = "sha256:2adc8be4fb285fbcd6d894298db3b52c177e74b6674eda3a76bd36be3292a3db", size = 67740, upload-time = "2026-09-14T14:18:04.717Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0d/117a771a2bb91df334b66bf4da14cd02f21aefbcfe53180f336ce55e8f90/httpcore2-2.13.0-py3-none-any.whl", hash = "sha256:35ae5be347aa40467b4a5dc032ac67ebb6d27189fc97e8cebcf99616f6a1bb9e", size = 83162, upload-time = "2026-09-14T14:18:02.529Z" }, ] [[package]] @@ -1764,7 +1764,7 @@ wheels = [ [[package]] name = "httpx2" -version = "2.12.0" +version = "2.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform != 'emscripten'" }, @@ -1774,9 +1774,9 @@ dependencies = [ { name = "truststore", marker = "sys_platform != 'emscripten'" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/a0/e9deef4654132857b5a5dbe4eddd0ac59c2814500e11f2f5044cd81103ee/httpx2-2.13.0.tar.gz", hash = "sha256:81bd07dc67a3701729ef1f777a3c00c915d4539604fdb5afd327f8682f6b7b44", size = 100290, upload-time = "2026-09-14T14:18:05.486Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d1/a0c72b0e006df654709fbc366cc5bcb53e5aee13e1e3395152c6dd293376/httpx2-2.13.0-py3-none-any.whl", hash = "sha256:fc12720cedf72faa26cca6b4ca394e05c894e7d7933fc45cafe767960804e49a", size = 95565, upload-time = "2026-09-14T14:18:03.553Z" }, ] [[package]] @@ -5480,15 +5480,15 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.52.4" +version = "0.53.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f2/0f/3f86e61397dd33bf2ccf28188c40db6a740658aeebbbf6e7dbc101a1f487/uvicorn-0.52.4.tar.gz", hash = "sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86", size = 100627, upload-time = "2026-08-19T06:27:41.821Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/ad/04bbb797c84fc1f26cb171f7394716f4865ffb8d8c5e1eef42565c2dfa6b/uvicorn-0.53.0.tar.gz", hash = "sha256:a9356f0cb89b3b8621529c5d5eebd69bfe154f4c3f68b4cf2de47e45fa855c2e", size = 110881, upload-time = "2026-09-14T07:44:23.815Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/79/4a20b54ab0491485ccd8c077db2d39187c7f12b3e15485d38a7be37c81b4/uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1", size = 79871, upload-time = "2026-08-19T06:27:40.36Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/0eea75741ee812e9f598b687619ce2454f6c3a1c5cd21ea990ec6bd26f45/uvicorn-0.53.0-py3-none-any.whl", hash = "sha256:e8dca71ec86dce5f04e333f0d56cdedf942446e6643b9cea1af0d6d3a02cb03e", size = 87081, upload-time = "2026-09-14T07:44:22.179Z" }, ] [[package]] From cd99aed7206532f11c40efbe28c810d3f83ceea6 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Tue, 15 Sep 2026 09:11:13 +0200 Subject: [PATCH 102/120] LCORE-4124: Unnecessary assignment to response before return statement --- src/app/endpoints/responses.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/endpoints/responses.py b/src/app/endpoints/responses.py index 33dadc561..5e5ae3e89 100644 --- a/src/app/endpoints/responses.py +++ b/src/app/endpoints/responses.py @@ -1458,7 +1458,7 @@ async def handle_non_streaming_response( tools, configuration.rag_id_mapping, ) - response = ResponsesResponse.model_validate( + return ResponsesResponse.model_validate( { **response_dict, "safety_identifier": api_params.safety_identifier, @@ -1468,4 +1468,3 @@ async def handle_non_streaming_response( "output_text": output_text, } ) - return response From f9c6ce0e4c01c92ad3998f353f198631beb26ac9 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Tue, 15 Sep 2026 09:06:33 +0200 Subject: [PATCH 103/120] LCORE-3819: Refactored transcripts into its own module --- src/transcripts/README.md | 6 ++++++ src/{utils => transcripts}/transcripts.py | 0 src/utils/README.md | 4 ---- src/utils/query.py | 8 ++++---- tests/unit/transcripts/README.md | 10 ++++++++++ tests/unit/transcripts/__init__.py | 1 + tests/unit/{utils => transcripts}/test_transcripts.py | 10 +++++----- tests/unit/utils/README.md | 4 ---- 8 files changed, 26 insertions(+), 17 deletions(-) create mode 100644 src/transcripts/README.md rename src/{utils => transcripts}/transcripts.py (100%) create mode 100644 tests/unit/transcripts/README.md create mode 100644 tests/unit/transcripts/__init__.py rename tests/unit/{utils => transcripts}/test_transcripts.py (94%) diff --git a/src/transcripts/README.md b/src/transcripts/README.md new file mode 100644 index 000000000..49d64bcb2 --- /dev/null +++ b/src/transcripts/README.md @@ -0,0 +1,6 @@ +# List of source files stored in `src/transcripts` directory + +## [transcripts.py](transcripts.py) + +Transcript handling. + diff --git a/src/utils/transcripts.py b/src/transcripts/transcripts.py similarity index 100% rename from src/utils/transcripts.py rename to src/transcripts/transcripts.py diff --git a/src/utils/README.md b/src/utils/README.md index fb2dceb04..efd2169da 100644 --- a/src/utils/README.md +++ b/src/utils/README.md @@ -128,10 +128,6 @@ Pre-LLM-call token estimation. Utility functions for formatting and parsing MCP tool descriptions. -## [transcripts.py](transcripts.py) - -Transcript handling. - ## [types.py](types.py) Common types for the project. diff --git a/src/utils/query.py b/src/utils/query.py index 04a3aa308..0ba27d6c5 100644 --- a/src/utils/query.py +++ b/src/utils/query.py @@ -31,14 +31,14 @@ from models.common.turn_summary import TurnSummary from models.config import Action from models.database.conversations import UserConversation, UserTurn -from utils.quota_utils import consume_tokens -from utils.suid import is_moderation_id, normalize_conversation_id -from utils.token_counter import TokenCounter -from utils.transcripts import ( +from transcripts.transcripts import ( create_transcript, create_transcript_metadata, store_transcript, ) +from utils.quota_utils import consume_tokens +from utils.suid import is_moderation_id, normalize_conversation_id +from utils.token_counter import TokenCounter logger = get_logger(__name__) diff --git a/tests/unit/transcripts/README.md b/tests/unit/transcripts/README.md new file mode 100644 index 000000000..d1e4b2e24 --- /dev/null +++ b/tests/unit/transcripts/README.md @@ -0,0 +1,10 @@ +# List of source files stored in `tests/unit/transcripts` directory + +## [__init__.py](__init__.py) + +Init of tests/unit/transcripts. + +## [test_transcripts.py](test_transcripts.py) + +Unit tests for functions defined in transcripts module. + diff --git a/tests/unit/transcripts/__init__.py b/tests/unit/transcripts/__init__.py new file mode 100644 index 000000000..4250e0b23 --- /dev/null +++ b/tests/unit/transcripts/__init__.py @@ -0,0 +1 @@ +"""Init of tests/unit/transcripts.""" diff --git a/tests/unit/utils/test_transcripts.py b/tests/unit/transcripts/test_transcripts.py similarity index 94% rename from tests/unit/utils/test_transcripts.py rename to tests/unit/transcripts/test_transcripts.py index fced39ace..1a11a2566 100644 --- a/tests/unit/utils/test_transcripts.py +++ b/tests/unit/transcripts/test_transcripts.py @@ -1,4 +1,4 @@ -"""Unit tests for functions defined in utils.transcripts module.""" +"""Unit tests for functions defined in transcripts module.""" import hashlib @@ -7,7 +7,7 @@ from configuration import AppConfig from models.api.requests import QueryRequest from models.common.turn_summary import ToolCallSummary, ToolResultSummary, TurnSummary -from utils.transcripts import ( +from transcripts.transcripts import ( construct_transcripts_path, create_transcript, create_transcript_metadata, @@ -39,7 +39,7 @@ def test_construct_transcripts_path(mocker: MockerFixture) -> None: cfg = AppConfig() cfg.init_from_dict(config_dict) # Update configuration for this test - mocker.patch("utils.transcripts.configuration", cfg) + mocker.patch("transcripts.transcripts.configuration", cfg) user_id = "user123" conversation_id = "123e4567-e89b-12d3-a456-426614174000" @@ -59,12 +59,12 @@ def test_store_transcript( # pylint: disable=too-many-locals """Test the store_transcript function.""" mocker.patch("builtins.open", mocker.mock_open()) mocker.patch( - "utils.transcripts.construct_transcripts_path", + "transcripts.transcripts.construct_transcripts_path", return_value=mocker.MagicMock(), ) # Mock the JSON to assert the data is stored correctly - mock_json = mocker.patch("utils.transcripts.json") + mock_json = mocker.patch("transcripts.transcripts.json") # Mock parameters user_id = "user123" diff --git a/tests/unit/utils/README.md b/tests/unit/utils/README.md index f18d2cca0..3431784c5 100644 --- a/tests/unit/utils/README.md +++ b/tests/unit/utils/README.md @@ -112,10 +112,6 @@ Unit tests for utils/token_estimator. Unit tests for tool_formatter utilities. -## [test_transcripts.py](test_transcripts.py) - -Unit tests for functions defined in utils.transcripts module. - ## [test_types.py](test_types.py) Unit tests for functions and types defined in utils/types.py. From ecba14af816e443a65a7d5b1be1fdf8d82698666 Mon Sep 17 00:00:00 2001 From: Andrej Simurka Date: Tue, 15 Sep 2026 11:59:28 +0200 Subject: [PATCH 104/120] remove safety identifier response workarounds Co-authored-by: Cursor --- src/app/endpoints/responses.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/app/endpoints/responses.py b/src/app/endpoints/responses.py index b1dc2a96f..6dce7a11f 100644 --- a/src/app/endpoints/responses.py +++ b/src/app/endpoints/responses.py @@ -813,9 +813,7 @@ async def handle_streaming_response( ) try: response = await context.client.responses.create( - **api_params.model_dump( - exclude_none=True, exclude={"safety_identifier"} - ) + **api_params.model_dump(exclude_none=True) ) generator = response_generator( stream=cast(AsyncIterator[OpenAIResponseObjectStream], response), @@ -1138,9 +1136,6 @@ async def response_generator( chunk_dict["response"]["conversation"] = normalize_conversation_id( api_params.conversation ) - chunk_dict["response"][ - "safety_identifier" - ] = api_params.safety_identifier _sanitize_response_dict( chunk_dict["response"], configured_mcp_labels, @@ -1351,9 +1346,7 @@ async def handle_non_streaming_response( api_response = cast( OpenAIResponseObject, await context.client.responses.create( - **api_params.model_dump( - exclude_none=True, exclude={"safety_identifier"} - ) + **api_params.model_dump(exclude_none=True) ), ) _record_response_inference_result( @@ -1463,7 +1456,6 @@ async def handle_non_streaming_response( response = ResponsesResponse.model_validate( { **response_dict, - "safety_identifier": api_params.safety_identifier, "available_quotas": available_quotas, "conversation": normalize_conversation_id(api_params.conversation), "completed_at": int(completed_at.timestamp()), From fb66a6cbf9d8e2e4623d3c1ce1f7cf8d22c7d706 Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Thu, 10 Sep 2026 17:03:52 +0200 Subject: [PATCH 105/120] LCORE-3386: align prompt guardrails spec with the shipped shield-based design The prompt guardrails spec described a standalone `guardrails:` config section with separate detectors and rules, a `src/guardrails/` package, a `DetectorBackend` protocol and a structured `ScreeningItem` payload. The detector framework that shipped under LCORE-3389 (PR #2580) took a different route: Granite Guardian is a shield type in the existing `shields:` list, configured through `GraniteGuardianShieldConfiguration`, `GraniteGuardianConfig` and `RiskDefinition`, and evaluated by an `AbstractSafetyCapability`. The spec was not updated at the time, so the tickets implementing the input and output points were being reviewed against a design that no longer matched the code. Rewrite the What, Requirements, Architecture, acceptance test surface, aspect-specific concerns and implementation suggestions sections to describe the shipped design: the shield's risk selection by point, the Granite Guardian 4.1 judge prompt, logprob-based scoring against the per-risk threshold, the two evaluation paths (run_shield_moderation_v2 before RAG on the Responses-based endpoints, agent capabilities on the agent-based endpoints), and the fail-closed error handling as implemented. Keep the requirements that still apply and name the ticket that covers each open one: concurrent evaluation and per-risk latency (LCORE-3390), advisory risks via a new `blocking` flag and streaming checkpoints (LCORE-3391), the validation-error metric that currently has no callers (LCORE-4089), and skipping RAG, the main LLM call and the topic-summary call for input-blocked requests on /v1/query and /v1/streaming_query (LCORE-4090). R6 is extended to cover topic-summary calls and RAG documents in the response, because the in-agent path currently returns both for blocked queries. Move capabilities of the original design that the shipped configuration does not provide into a new "Deferred from the original design" section: the separate config section and backend protocol, the openai_moderations and llama_stack_shields backends, out-of-the-box risk ids, the boolean verdict without a threshold, fail-open and refusal-shaped detector failures, api_key_path, the concurrent input execution mode and a global violation message. Record model selection and guardian token accounting as open questions, note that the `enable_thinking` docstring points at a `ModerationConfig.thinking_enabled` setting that does not exist, and add a changelog row explaining the revision. --- .../prompt-guardrails/prompt-guardrails.md | 652 +++++++++--------- 1 file changed, 340 insertions(+), 312 deletions(-) diff --git a/docs/design/prompt-guardrails/prompt-guardrails.md b/docs/design/prompt-guardrails/prompt-guardrails.md index cbe86b33e..e07e6eb8d 100644 --- a/docs/design/prompt-guardrails/prompt-guardrails.md +++ b/docs/design/prompt-guardrails/prompt-guardrails.md @@ -2,361 +2,361 @@ | | | |--------------------|-------------------------------------------| -| **Date** | 2026-07-20 | +| **Date** | 2026-07-20 (revised 2026-09-10) | | **Component** | lightspeed-stack | | **Authors** | Maxim Svistunov | | **Feature** | [LCORE-230](https://redhat.atlassian.net/browse/LCORE-230) | +| **Epic** | [LCORE-3386](https://redhat.atlassian.net/browse/LCORE-3386) | | **Spike** | [LCORE-2657](https://redhat.atlassian.net/browse/LCORE-2657) | -| **Links** | [Spike doc](prompt-guardrails-spike.md), [OWASP LLM01](https://genai.owasp.org/llmrisk/llm01-prompt-injection/) | +| **Links** | [Spike doc](prompt-guardrails-spike.md), [Shields guide](../../user_doc/shields_guide.md), [OWASP LLM01](https://genai.owasp.org/llmrisk/llm01-prompt-injection/) | + +> **Revision note (2026-09-10).** The first version of this document proposed +> a standalone `guardrails:` configuration section and a `src/guardrails/` +> package with pluggable detector backends. The configuration that shipped +> under LCORE-3389 (PR #2580) instead builds guardrails as a **shield type** +> inside the existing shields framework. This revision describes that +> architecture. Capabilities of the original design that the shipped +> configuration does not provide are listed in +> [Deferred from the original design](#deferred-from-the-original-design); +> requirements that are still open name the ticket that covers them. ## What -An optional, config-driven guardrails layer owned by lightspeed-stack. -Deployers declare **detectors** (guardian-model endpoints reachable through -OpenAI-compatible APIs — Granite Guardian on vLLM/RHAIIS, any -`/v1/moderations` service, or, transitionally, OGX shields) and -**rules** (an out-of-the-box risk id or a custom risk definition, bound to -one or more guardrail **points**: `input`, `output`, `tool_content`, with a -blocking or advisory posture). The layer runs the applicable rules in -parallel at each point of the request lifecycle and blocks (or annotates) -requests whose content is flagged. +An optional, config-driven guardrails layer owned by lightspeed-stack and +built on its shields framework. A deployer adds a `granite_guardian` entry to +the top-level `shields:` list in `lightspeed-stack.yaml`. The entry names an +IBM Granite Guardian model reachable through an OpenAI-compatible API and +declares a list of **risks**. Each risk carries a custom risk definition, a +score threshold, a violation message, and the guardrail **points** where it +applies: `input`, `output`, or `tool`. At each point the applicable risks are +evaluated, and content that is flagged is blocked with that risk's violation +message. ## Why -Prompt injection is OWASP's #1 LLM risk. lightspeed-stack today moderates -only *input*, only through OGX shields — an OGX API surface upstream -has deleted in OGX 1.x — with no lightspeed-stack-side configuration, no -output or tool-content coverage, no Granite Guardian support, and no custom -risk definitions. Ask Red Hat's migration to Lightspeed Core -([LCORE-2253](https://redhat.atlassian.net/browse/LCORE-2253)) is blocked -on exactly those capabilities (they run parallel multi-risk Granite -Guardian screening with custom risks in production today). This feature -provides them generically, in a form that survives the planned OGX -phase-out. +Prompt injection is OWASP's #1 LLM risk. Before this feature, lightspeed-stack +moderated only *input*, and only through OGX shields -- an API surface that +OGX 1.x removed -- with no lightspeed-stack-side configuration, no output or +tool-content coverage, no Granite Guardian support, and no custom risk +definitions. Ask Red Hat's migration to Lightspeed Core +([LCORE-2253](https://redhat.atlassian.net/browse/LCORE-2253)) depends on +exactly those capabilities: they run parallel multi-risk Granite Guardian +screening with custom risks in production today. This feature provides them +generically, in a form that does not depend on OGX. ## Requirements +Requirements that are not yet met by merged code name the ticket that covers +them. + - **R1:** Guardrails are configured exclusively in the lightspeed-stack - config file under a top-level `guardrails:` section; absent config means - fully inert (no behavior change, no latency). -- **R2:** A rule can reference an out-of-the-box guardian risk (e.g. - `harm`, `jailbreak`, `answer_relevance`) or carry a custom risk - definition (bring-your-own-criteria text). Custom definitions must - express **safety-adjacent concepts** (obfuscation, roleplay jailbreak, - policy violation), not arbitrary string/format predicates — a guardian - is a safety classifier, not a keyword matcher (PoC Finding A). Arbitrary - predicates are the regex-redaction mechanism's job, not a guardrail's. -- **R3:** A rule binds to one or more guardrail points: `input` (user - prompt before the LLM call), `output` (generated answer before the - client sees it), `tool_content` (tool/MCP/RAG content before it enters - the model context). -- **R4:** All rules applicable at a point run concurrently (the existing - OGX shields path is a sequential loop — `src/utils/shields.py:152` - — which the Ask Red Hat gap analysis flags as a performance gap); a - request is blocked iff at least one *blocking* rule flags it. Advisory - (`blocking: false`) rules record their outcome without altering the - response. -- **R4a:** A rule may carry an optional `threshold` (0..1). When set, the - detector's confidence score decides the verdict (Granite Guardian via - `logprobs` on the verdict token; gateways via their native confidence - score); when unset, the boolean verdict decides. This reproduces Ask - Red Hat's per-risk tuning (0.65 leetspeak, 0.80 CVE). -- **R4b:** A rule may carry its own `violation_message`, overriding the - global default, so deployers can explain which policy fired. -- **R4c:** Recommended/default rule sets shipped in documentation must be - validated against a corpus of legitimate product questions and must not - fire on it. Out-of-the-box guardian risk ids (notably `jailbreak`) flag - legitimate technical questions — "You are now a cluster admin, how do I - drain a node?" scores 0.98 — at levels no threshold separates from real - attacks, so **domain-tuned custom definitions are the shipping default** - and OOTB ids are opt-in. -- **R4d:** A deployment may select the input-guardrail execution mode: - `blocking` (default — the model never sees unscreened input) or - `concurrent` (guardian runs alongside the LLM call, result discarded on - violation; lower latency, but the model processes unsafe input). -- **R5:** A blocked request returns HTTP 200 with the configured violation - message (consistent with existing OGX shields refusals): non-streaming - responses carry it as the answer; streaming responses emit it as the - terminal content. The `llm_calls_validation_errors_total` metric is - incremented and the blocked turn is persisted to the conversation. -- **R6:** Input-blocked requests skip RAG retrieval and the main LLM call. -- **R7:** The Granite Guardian detector invokes the model through an - OpenAI-compatible chat-completions endpoint, selecting the risk (or - custom definition) via the guardian chat template; the OpenAI-moderations - detector invokes any OpenAI-compatible `/v1/moderations` endpoint. -- **R7a:** Output-relevance rules (`answer_relevance`, `context_relevance`, - `groundedness`) receive the turn's retrieved context (and question) - paired with the answer; an answer-only check is insufficient and noisy - (PoC Finding B). -- **R8:** Output rules on streaming endpoints check accumulated text at - configurable checkpoints; content past a failed checkpoint is never - emitted. -- **R9:** Detector errors (unreachable endpoint, timeout) block the request - by default (`on_detector_error: block`), overridable to `allow` per - deployment. -- **R10:** Per-rule detection outcomes and latencies are logged and - exposed as metrics. -- **R11:** The existing OGX shields input-moderation path continues - to work unchanged when `guardrails:` is not configured; both may run - side by side during migration. + config file, as a `granite_guardian` entry in the top-level `shields:` + list. Without such an entry the feature is fully inert: no behavior change + and no added latency. +- **R2:** A risk is defined by custom criteria text (`description`) that is + passed to Granite Guardian. Definitions must express **safety-adjacent + concepts** (obfuscation, roleplay jailbreak, policy violation), not + arbitrary string or format predicates: a guardian is a safety classifier, + not a keyword matcher (PoC Finding A). Arbitrary predicates are the job of + the redaction shield. +- **R3:** A risk binds to one or more guardrail points: `input` (the user + prompt before the LLM call), `output` (the generated answer before the + client sees it), and `tool` (tool, MCP, or RAG content before it enters the + model context). +- **R4:** All risks applicable at a point are evaluated concurrently, with + per-risk latency logged. A request is blocked if at least one *blocking* + risk flags it. (Concurrency and latency logging: LCORE-3390. The shields + loop in `src/utils/shields.py` is sequential, which the Ask Red Hat gap + analysis flags as a performance gap.) +- **R4a:** Each risk has a `threshold` between 0 and 1 (default 0.65). The + verdict is decided by Granite Guardian's confidence score, derived from the + logprobs of its verdict token, which reproduces Ask Red Hat's per-risk + tuning (0.65 leetspeak, 0.80 CVE). +- **R4b:** Each risk carries its own `violation_message`, so deployers can + explain which policy fired. +- **R4c:** Recommended rule sets shipped in documentation must be validated + against a corpus of legitimate product questions and must not fire on it. + Generic phrasings of risks such as "jailbreak" flag legitimate technical + questions -- "You are now a cluster admin, how do I drain a node?" scores + 0.98 -- at levels no threshold separates from real attacks, so + **domain-tuned custom definitions are the shipping default** (LCORE-3394). +- **R4e:** A risk can be **advisory**: it records its outcome without + altering the response. Advisory risks are required for output relevance + checks. (Needs a `blocking` flag on `RiskDefinition`, default true: + LCORE-3391.) +- **R5:** A blocked request returns HTTP 200 with the violation message: + non-streaming responses carry it as the answer, streaming responses emit + it as the terminal content. The blocked turn is persisted to the + conversation and the `llm_calls_validation_errors_total` metric is + incremented. (The metric currently has no callers anywhere: LCORE-4089.) +- **R6:** A request blocked at the `input` point performs no RAG retrieval, + no main LLM call and no topic-summary call, and its response carries no RAG + chunks or referenced documents. (Met on `/v1/responses` and `/rlsapi`; not + yet on `/v1/query` and `/v1/streaming_query`: LCORE-4090.) +- **R7:** Granite Guardian is invoked through an OpenAI-compatible + chat-completions endpoint, using its client-side judge prompt with the + risk's criteria. +- **R7a:** Output relevance risks (answer relevance, context relevance, + groundedness) receive the turn's retrieved context and question alongside + the answer; an answer-only check is insufficient and noisy (PoC Finding B). + The shield's evaluation interface currently takes plain text, so the + payload for relevance checks is to be designed in LCORE-3391. +- **R8:** Output risks on streaming endpoints check accumulated text at + checkpoints; content past a failed checkpoint is never emitted + (LCORE-3391). +- **R9:** Guardian errors (unreachable endpoint, timeout, unparseable + verdict) fail closed: the request is not served. +- **R10:** Per-risk outcomes and latencies are logged and exposed as metrics + (LCORE-3390 for input, LCORE-3391 for output). +- **R11:** The other shields (`question_validity`, `redaction`) continue to + work unchanged, and may be configured alongside `granite_guardian`. ## Use Cases - **U1:** As a Lightspeed product team (e.g. Ask Red Hat), I want to - declare my guardian model endpoint and my product's risk set (OOTB + - custom definitions) in the LCS config file, so that my product is - protected without custom code. -- **U2:** As a deployer, I want prompts that attempt jailbreak/injection + declare my guardian model endpoint and my product's risk definitions in + the LCS config file, so that my product is protected without custom code. +- **U2:** As a deployer, I want prompts that attempt jailbreak or injection blocked before they reach the LLM, so that the assistant cannot be subverted. -- **U3:** As a deployer, I want generated answers checked (e.g. harm, - answer relevance) before delivery, so that unsafe or off-context output - never reaches users. +- **U3:** As a deployer, I want generated answers checked (e.g. harm, answer + relevance) before delivery, so that unsafe or off-context output never + reaches users. - **U4:** As a deployer of an MCP-enabled assistant, I want tool and RAG content screened before the model consumes it, so that indirect prompt injection via third-party content is caught. -- **U5:** As an SRE, I want per-rule outcomes and latencies in metrics, so - that I can observe block rates and tune thresholds/rules. -- **U6:** As a security engineer, I want the service to fail closed when - the guardian endpoint is down, so that protection cannot silently lapse. +- **U5:** As an SRE, I want per-risk outcomes and latencies in logs and + metrics, so that I can observe block rates and tune thresholds. +- **U6:** As a security engineer, I want the service to fail closed when the + guardian endpoint is down, so that protection cannot silently lapse. ## Architecture ### Overview ```text - ┌────────────────────────────── lightspeed-stack ──────────────────────────────┐ - │ │ - user query ─┼─► input rules ──blocked──► 200 refusal (skip RAG + LLM; persist turn) │ - │ (parallel) │ - │ │ passed │ - │ ▼ │ - │ RAG retrieval ─► LLM call (Responses API) │ - │ │ ▲ │ - │ tool results │ tool_content rules gate each result │ - │ └────────┘ (flagged content never enters context) │ - │ ▼ │ - │ output rules ──blocked──► refusal replaces/terminates answer │ - │ (checkpointed when streaming) │ - │ │ passed │ - └──────┼───────────────────────────────────────────────────────────────────────┘ - ▼ - response - all rule checks ──► DetectorBackend ──► guardian model - (Guardian chat template (vLLM / RHAIIS / - or /v1/moderations) Ollama / gateway) + ┌─────────────────────────── lightspeed-stack ────────────────────────────┐ + │ │ + user query ─┼─► input sanitization ─► input risks ──blocked──► 200 refusal │ + │ (shield) (skip RAG + LLM; persist turn) │ + │ │ passed │ + │ ▼ │ + │ RAG retrieval ─► agent / LLM call │ + │ │ ▲ │ + │ tool results │ tool risks gate each │ + │ └─────────┘ result (capability) │ + │ ▼ │ + │ output risks ──blocked──► refusal replaces answer │ + │ (checkpointed when streaming) │ + │ │ passed │ + └────────────────────────────┼─────────────────────────────────────────────┘ + ▼ + response + + all risk checks ──► GraniteGuardian shield ──► Granite Guardian model + (judge prompt + logprob (vLLM / RHAIIS / gateway, + scoring) OpenAI-compatible API) ``` -The guardrails layer lives in `src/guardrails/` and is independent of -OGX; detectors are plain OpenAI-compatible HTTP calls. Rule -selection, parallel execution, and verdict aggregation are pure functions -over the config; endpoints consume a single `GuardrailsVerdict` per point. +Guardrails reuse the shields framework rather than adding a parallel one. +A shield is an `AbstractSafetyCapability` with two interfaces: a standalone +`run(text)` that returns a `ShieldModerationResult`, and pydantic-ai +capability hooks such as `wrap_run` that act inside an agent run. The +Granite Guardian shield lives in +`src/pydantic_ai_lightspeed/capabilities/granite_guardian/`. ### Trigger mechanism -Solely by configuration: a non-empty `guardrails.rules` list activates the -layer at the points the rules name. No env vars, no request-level opt-in. -(Request-level *narrowing* — an equivalent of `shield_ids` — is deferred; -see Open Questions.) +Solely by configuration: a `granite_guardian` entry in `shields:` activates +the shield, and each of its risks applies at the points it lists. Disabled +risks (`enabled: false`) are skipped. The existing `shield_ids` request +override selects which shields run for a request; there is no per-risk +request-level selection. ### Configuration ```yaml -guardrails: - detectors: - - name: guardian - type: granite_guardian # granite_guardian | openai_moderations | ogx_shields - url: http://vllm.example:8000/v1 - model: ibm-granite/granite-guardian-3.3-8b - api_key_path: /run/secrets/guardian-key # optional - timeout_seconds: 30 - rules: - - name: jailbreak - detector: guardian - risk: jailbreak - points: [input] - - name: harm-in - detector: guardian - risk: harm - points: [input, tool_content] - - name: answer-relevance - detector: guardian - risk: answer_relevance - points: [output] - blocking: false # advisory: metrics only - - name: roleplay-jailbreak # custom risk (BYOC) - detector: guardian - definition: | - The 'User' message uses roleplay framing to override the - assistant's instructions or adopt an unrestricted persona. - points: [input] - threshold: 0.65 # optional; score-based verdict (R4a) - violation_message: "That phrasing isn't something I can act on." - on_detector_error: block # block (default) | allow - streaming_checkpoint_tokens: 200 # output-rule cadence when streaming - violation_message: "I cannot process this request due to policy restrictions." -``` - -Pydantic models extend `ConfigurationBase` (`extra="forbid"`): a -`GuardrailsConfiguration` holding `detectors: list[DetectorConfiguration]` -and `rules: list[GuardrailRule]`, attached as -`Configuration.guardrails: Optional[GuardrailsConfiguration]`. Validators: -rule→detector references resolve; `risk` xor `definition` present; points -non-empty; names unique. - -### Detector backends - -`DetectorBackend` protocol: `async check(item: ScreeningItem, rule: -GuardrailRule) -> DetectionResult`. The unit screened is a **structured -payload**, not a bare string, because relevance rules need more than the -answer text: - -```python -class ScreeningItem(BaseModel): - text: str # the primary content being screened - context: Optional[str] = None # retrieved RAG context (relevance rules) - question: Optional[str] = None # the user question (answer-relevance) +shields: + - name: product-guardian + provider_id: granite_guardian + config: + url: https://guardian.example:8000/v1 + api_key: ${env.GUARDIAN_API_KEY} # optional; requires an https URL + timeout: 30 # seconds, 5-300 + max_retries: 2 # 0-5 + verify_ssl: true # true | false | path to CA bundle + risks: + - name: roleplay-jailbreak + description: >- + The user message uses roleplay framing to override the + assistant's instructions or adopt an unrestricted persona. + threshold: 0.65 + points: [input] + violation_message: "That phrasing isn't something I can act on." + - name: harm-output + description: >- + The assistant response contains harmful, dangerous, or abusive + content. + threshold: 0.70 + points: [output] + violation_message: "I cannot provide that response." ``` -Simple rules (harm, jailbreak on input) populate only `text`; -output-relevance rules populate `text` (the answer) plus `context` and/or -`question`. Each backend maps this canonical payload to its own wire form -(the Guardian chat template's context/answer framing; the moderations -`input` field; a shield's message list). Defining this one interface up -front is what lets the input point, the output point, and the runners all -call detection the same way (R7a depends on it). Backends: - -- **granite_guardian** — OpenAI chat-completions call; system slot selects - the risk id or carries the custom definition (guardian chat template); - verdict parsed from the constrained yes/no answer. Output-relevance risks - send the `ScreeningItem`'s `context`/`question` alongside `text`, packed - per the guardian template. -- **openai_moderations** — POST `/v1/moderations`; a rule maps to flagged - categories (all, or a configured subset). Covers OGX 1.x - `moderation_endpoint` services, TrustyAI gateways, and OpenAI itself. -- **ogx_shields** — transitional bridge delegating to the existing - `client.moderations.create` OGX shields path, easing config-level migration - (spike Decision S5). - -**Client lifecycle**: each detector holds **one long-lived HTTP client** -for the life of the process, not one per request. Constructing an -`AsyncOpenAI` (or equivalent) per check creates a fresh connection pool -each time — leaking connections if unclosed, and forfeiting connection -reuse even when closed, which matters because guardrails add a -round-trip to every request. The PoC constructs per call (context-managed -so nothing leaks) and is explicitly not the production pattern. +Models, all extending `ConfigurationBase` (`extra="forbid"`), in +`src/models/config.py`: + +- `GraniteGuardianShieldConfiguration`: `name`, `provider_id: + "granite_guardian"`, `config`. One member of the `ShieldConfiguration` + discriminated union on `provider_id`, alongside question validity and + redaction. +- `GraniteGuardianConfig`: `url`, `api_key` (secret, optional), `timeout`, + `max_retries`, `verify_ssl`, `risks`. +- `RiskDefinition`: `name`, `description`, `threshold` (default 0.65), + `enabled` (default true), `enable_thinking` (default false), `points` + (non-empty subset of `input`, `output`, `tool`), `violation_message`. + +The model docstring and field description for `enable_thinking` refer to a +`ModerationConfig.thinking_enabled` setting that does not exist; until that +is resolved, `enable_thinking` is the only control for think mode. + +### Granite Guardian shield + +- **Risk selection:** for a given point, the shield evaluates the enabled + risks whose `points` include that point. +- **Judge prompt:** each risk is sent as a chat-completions request that + combines the text under evaluation with Granite Guardian 4.1's client-side + judge block: the risk's criteria text, a yes/no scoring schema, and either + a no-think or a think preamble (`enable_thinking`). +- **Scoring:** the request asks for logprobs. The shield parses the response + through its `` and `` tags, takes the top logprobs of the + verdict token, and computes `p_risky = p(yes) / (p(yes) + p(no))`. The risk + is flagged when `p_risky >= threshold`. A response without logprobs or + without a parseable verdict raises an error, which is handled per R9. +- **Model:** the implementation targets `ibm-granite/granite-guardian-4.1-8b` + and does not currently expose the model name as configuration (see Open + Questions). +- **Client lifecycle:** the shield should hold **one long-lived HTTP client** + for the life of the process, not one per request. Constructing a client + per check creates a fresh connection pool each time -- leaking connections + if it is never closed, and forfeiting connection reuse even when it is -- + on a path that runs on every request. The first implementation constructs + the client each time the shield is built, which happens per request; this + is tracked in the LCORE-3390 review. ### Request lifecycle integration -- **Input**: next to the existing `run_shield_moderation` call in - `src/app/endpoints/query.py`, `streaming_query.py`, `responses.py`, - `rlsapi_v1.py` — the guardrails verdict feeds the same - `ShieldModerationResult` seam, so the blocked path (RAG skip, refusal, - turn persistence, metrics) is reused as-is. -- **Output**: non-streaming — single check between response retrieval and - `QueryResponse` assembly; streaming — checkpointed buffer-and-release in - the SSE generators (`src/utils/agents/streaming.py`, +- **Input, Responses-based endpoints (`/v1/responses`, `/rlsapi`):** + `run_shield_moderation_v2` runs before RAG retrieval. It sanitizes the + input, then calls each selected shield's `run()`; the first block returns + a `ShieldModerationBlocked`, and the endpoint's existing blocked path + handles the refusal, persistence and RAG skip. +- **Input, agent-based endpoints (`/v1/query`, `/v1/streaming_query`):** + shields are currently attached to the agent as capabilities and evaluated + in `wrap_run`, inside the agent run and therefore after RAG retrieval; the + pre-agent `run_shield_moderation` call on these endpoints is a stub that + always passes. LCORE-4090 moves input shields before RAG on these + endpoints, through the same `run_shield_moderation_v2` path, so that R6 + holds everywhere and each shield runs once per request. +- **Output (LCORE-3391):** non-streaming -- a single check between response + retrieval and response assembly; streaming -- checkpointed + buffer-and-release in the SSE generators (`src/utils/agents/streaming.py`, `src/utils/streaming_sse.py`). -- **Tool content**: a pydantic-ai capability (same mechanism as the - existing inert safety capabilities in - `src/pydantic_ai_lightspeed/capabilities/`) intercepts each tool result - before it re-enters the agent loop; flagged content is replaced by a - policy notice or aborts the turn per the rule's blocking flag. - -The guardrails module itself stays a thin, framework-agnostic library -(`content + rule → verdict`). Per reviewer note, the agent runners in -`src/runners/` are the natural place to invoke it — calling into -`src/guardrails/` from a runner is only a few lines, and keeps the -detection logic decoupled from any one execution path (query endpoint, -runner, or streaming generator). +- **Tool (LCORE-3392):** a capability hook intercepts each tool result before + it re-enters the agent loop; flagged content is replaced by a policy notice + or aborts the turn, per the risk's blocking posture. + +See [How shields apply at runtime](../../user_doc/shields_guide.md) for the +per-endpoint behavior of shields in general. ### API changes -None to request models in the core epic. Response behavior on block is the -established refusal shape. (A `guardrail_ids` request-narrowing field -analogous to `shield_ids` is an open question.) +None to request models. The response on block is the established refusal +shape. `GET /v1/shields` lists `granite_guardian` shields like any other. ### Error handling -Detector connectivity/timeout errors follow `on_detector_error`: -`block` (default) returns the refusal shape with a distinct log line and -metric label; `allow` logs a warning and proceeds. Config errors -(unresolvable detector reference, bad risk spec) fail startup validation. +Guardian connectivity errors, timeouts, and unparseable verdicts fail +closed. On the `run_shield_moderation_v2` path the error is mapped to an +HTTP error response; inside an agent run it propagates out of the run and is +returned as an HTTP error (non-streaming) or an `error` SSE event +(streaming). A configurable fail-open posture and a refusal-shaped response +for detector failures are deferred (see below). Configuration errors fail +startup validation. ### Security considerations -- Guardian endpoints and API keys are deployment secrets — keys are read - from files (`api_key_path`) per project convention, never inline. +- The guardian endpoint and its API key are deployment secrets. The API key + is a secret string in configuration; when an API key is set the endpoint + URL must use HTTPS, so the key is never sent in clear text. - Detection is risk reduction, not a security boundary: published bypasses - exist for classifier-based defenses. Layered posture (all three points + - least-privilege MCP config) is the mitigation; thresholds/risks are - deployment policy. -- Moderated content is sent to the guardian endpoint: deployers must place - detectors within the same trust boundary as the serving LLM. + exist for classifier-based defenses. A layered posture -- all three points + plus least-privilege MCP configuration -- is the mitigation; risk + definitions and thresholds are deployment policy. +- Moderated content is sent to the guardian endpoint, so deployers must + place it within the same trust boundary as the serving LLM. ### Migration / backwards compatibility -No `guardrails:` section ⇒ byte-identical behavior to today (R11). The -OGX shields path is untouched; its deprecation is deferred to the -OGX 1.x migration (LCORE-1099). The `ogx_shields` backend lets -deployments move their config to the new schema before OGX migrates. +No `granite_guardian` shield configured means behavior is unchanged (R1). +Existing `question_validity` and `redaction` shields are unaffected (R11). ## Acceptance test surface | Req | Observable behavior | Verified by | |-----|---------------------|-------------| -| R1 | No `guardrails:` config ⇒ responses and latency unchanged | e2e | -| R2 | OOTB risk blocks a matching prompt; custom definition blocks its target phrasing | e2e | -| R7a | Relevance rule receives context+answer; answer-only run flagged as misconfiguration in review | integration | -| R3 | A rule with `points: [output]` never fires on input, and vice versa | integration | -| R4 | Two input rules ⇒ both detector calls observed concurrently; advisory rule never alters response | integration | -| R4a | Same content flips verdict across a threshold boundary (e.g. 0.6 vs 0.9); unset threshold falls back to boolean verdict | integration | -| R4b | Rule with its own `violation_message` returns that text, not the global default | e2e | -| R4c | Documented recommended rule set produces zero blocks on the legitimate-question corpus | e2e / tuning fixture | -| R4d | `concurrent` mode returns the same verdict as `blocking` for the same input, with lower wall-clock | integration | +| R1 | No `granite_guardian` shield ⇒ responses and latency unchanged | e2e | +| R2 | A custom risk definition blocks its target phrasing and passes a benign one | e2e | +| R3 | A risk with `points: [output]` never fires on input, and vice versa | integration | +| R4 | Two input risks ⇒ both guardian calls observed concurrently; per-risk latency logged | integration | +| R4a | Same content flips verdict across a threshold boundary (e.g. 0.6 vs 0.9) | integration | +| R4b | A risk's own `violation_message` is returned when it fires | e2e | +| R4c | Documented recommended risk set produces zero blocks on the legitimate-question corpus | e2e / tuning fixture | +| R4e | An advisory risk never alters the response; its outcome appears in metrics | integration | | R5 | Blocked query ⇒ HTTP 200, violation message as answer, metric incremented, turn persisted | e2e | -| R6 | Input-blocked query produces no RAG retrieval and no main-LLM call | integration | -| R7 | Guardian receives risk id / definition in the system slot; moderations backend hits `/v1/moderations` | integration | +| R6 | Input-blocked query ⇒ no RAG retrieval, no main-LLM or topic-summary call, no RAG documents in the response | integration | +| R7 | Guardian request carries the judge block with the risk's criteria and requests logprobs | integration | | R8 | Streaming: flagged checkpoint ⇒ refusal emitted, withheld text never sent | e2e | -| R9 | Detector down ⇒ refusal (default) / pass-through (`allow`) | e2e | -| R10 | Per-rule outcome + latency present in logs and metrics | integration | -| R11 | OGX shields-only deployment behaves exactly as before the feature | e2e | +| R9 | Guardian down or unparseable ⇒ request not served | integration / e2e | +| R10 | Per-risk outcome and latency present in logs and metrics | integration | +| R11 | Question-validity and redaction shields behave as before when a Guardian shield is added | e2e | ## Aspect-specific concerns ### Latency and Cost -Each blocking rule adds one guardian inference to the critical path; -parallel execution makes the per-point cost ≈ the slowest single check -(Guardian-8B on GPU: high tens to low hundreds of ms; small CPU models: -lower). Input and output points each add at most one such round; -`tool_content` multiplies by tool-call count — deployers control exposure -via rule→point bindings, and per-rule latency metrics (R10) make the cost -observable. PoC latency measurements: see the spike doc's PoC results. +Each risk adds one guardian inference on its point's critical path. With +concurrent evaluation (R4) the cost per point is roughly the slowest single +check (Guardian 8B on GPU: high tens to low hundreds of ms); evaluated +sequentially it grows linearly with the number of risks. The `tool` point +multiplies by the number of tool calls; deployers control exposure through +point bindings, and per-risk latency (R10) makes the cost observable. +Guardian token usage is not counted against user quota or reported token +counts; it should at least be logged per risk. PoC latency measurements: +see the spike doc's PoC results. ### Observability -Per-rule structured logs (rule, point, verdict, latency, raw verdict -text at debug); metrics: existing `llm_calls_validation_errors_total` on -block, plus per-rule outcome/latency counters and histograms. Detector -errors get a distinct metric label to drive alerting (fail-closed events -are page-worthy). +Per-risk structured logs (risk, point, verdict, score, latency; raw verdict +text at debug level). Metrics: `llm_calls_validation_errors_total` on block +(LCORE-4089), plus per-risk outcome and latency counters and histograms. +Guardian errors get a distinct log line and metric label, because fail-closed +events are page-worthy. ### Failure modes -- Guardian endpoint down ⇒ R9 posture (default: block; alert fires). -- Guardian misbehaving (non-yes/no output) ⇒ treated as not-flagged for - advisory rules and per `on_detector_error` for blocking rules - (unparseable verdict ≈ detector error). -- Slow detector ⇒ per-detector timeout bounds the stall; timeout ⇒ R9. -- Config drift (rule names a removed detector) ⇒ startup validation error. +- Guardian endpoint down or timing out ⇒ fail closed (R9); the configured + `timeout` and `max_retries` bound the stall. +- Guardian output without the expected ``/`` structure or + without logprobs ⇒ treated as a guardian error (R9). +- Configuration drift (for example an unknown point name) ⇒ startup + validation error. ### Runbook / oncall implications -New alert: detector-error rate (fail-closed blocks). Recovery: restore the -guardian endpoint or temporarily set `on_detector_error: allow` /remove -rules (explicit, logged policy change). Block-rate dashboards distinguish -policy blocks (working as intended) from error blocks. +New alert: guardian error rate (fail-closed requests). Recovery: restore the +guardian endpoint, or remove or disable the affected risks (`enabled: +false`) as an explicit, logged policy change. Block-rate dashboards should +distinguish policy blocks (working as intended) from error-driven failures. ## Implementation Suggestions @@ -364,54 +364,81 @@ policy blocks (working as intended) from error blocks. | File | What to do | |------|------------| -| `src/models/config.py` | Add `GuardrailsConfiguration` + sub-models; attach to `Configuration` | -| `src/guardrails/` (new) | Models, `DetectorBackend` protocol, backends, parallel runner | -| `src/app/endpoints/query.py` (+streaming, responses, rlsapi) | Input-point call feeding the `ShieldModerationResult` seam | -| `src/utils/agents/streaming.py`, `src/utils/streaming_sse.py` | Output checkpoints in SSE generators | -| `src/pydantic_ai_lightspeed/capabilities/` | Tool-content gating capability; wire via `_agent_capabilities()` | -| `src/metrics/` | Per-rule outcome/latency instruments | -| `docs/user_doc/`, `examples/` | Deployer guide + validated config example | +| `src/models/config.py` | `GraniteGuardianShieldConfiguration`, `GraniteGuardianConfig`, `RiskDefinition` (shipped); add `blocking` to `RiskDefinition` (LCORE-3391) | +| `src/pydantic_ai_lightspeed/capabilities/granite_guardian/` | The shield: risk selection, judge prompt, logprob scoring, `run()` and capability hooks | +| `src/utils/shields.py` | `run_shield_moderation_v2` and `build_shield`; concurrent risk evaluation | +| `src/app/endpoints/query.py`, `streaming_query.py` | Input shields before RAG via `run_shield_moderation_v2` (LCORE-4090) | +| `src/utils/pydantic_ai_helpers.py` | Capabilities attached to agents; keep the tool point here, move input out (LCORE-4090) | +| `src/utils/agents/streaming.py`, `src/utils/streaming_sse.py` | Output checkpoints in the SSE generators (LCORE-3391) | +| `src/metrics/` | Per-risk outcome and latency instruments; call the validation-error metric (LCORE-4089) | +| `docs/user_doc/`, `examples/` | Deployer guide and validated config example (LCORE-3394) | ### Insertion point detail -The input hook mirrors the PoC: after `run_shield_moderation(...)` in each -endpoint, when the verdict blocks, construct `ShieldModerationBlocked` -(message, synthetic moderation id, refusal response) — every downstream -branch already handles it. The tool-content capability follows the -`QuestionValidity` capability's interception pattern -(`src/pydantic_ai_lightspeed/capabilities/question_validity/_capability.py`) +The input point uses the shields path that Responses-based endpoints already +use: `run_shield_moderation_v2` before `build_rag_context`, returning a +`ShieldModerationBlocked` that every downstream branch already handles. The +tool point follows the question-validity capability's interception pattern +(`src/pydantic_ai_lightspeed/capabilities/question_validity/_capability.py`), applied to tool results rather than the user prompt. ### Config pattern -Follow the project's Configuration conventions (see -[CLAUDE.md](../../../CLAUDE.md) — Configuration section); schema and YAML -example above. Regenerate `docs/openapi.json` and config docs after -attaching the section. +Follow the project's configuration conventions (see +[CLAUDE.md](../../../CLAUDE.md), Configuration section). Regenerate +`docs/devel_doc/openapi.json` and the config docs after changing the models. ### Test patterns -- Unit/integration tests need **no real guardian**: a scripted - OpenAI-compatible mock (respond yes/no per marker phrases) exercises - every layer behavior deterministically. -- e2e needs a guardian stand-in the CI environment can run: either the - mock detector as a service, or a small real model where resources allow - — decide in the step-definitions ticket against CI constraints. -- Concurrency: assert parallelism (not sequencing) of multi-rule points - via call-timestamp capture in the mock. +- Unit and integration tests need **no real guardian**: a scripted + OpenAI-compatible mock that returns a `` verdict with logprobs per + marker phrase exercises every shield behavior deterministically. +- e2e needs a guardian stand-in the CI environment can run: either the mock + as a service or a small real model where resources allow; decide in the + step-definitions ticket (LCORE-3388) against CI constraints. +- Concurrency: assert that risks at one point are evaluated in parallel, not + in sequence, by capturing call timestamps in the mock. +- Failure posture: pin the fail-closed behavior on both the + `run_shield_moderation_v2` path and the in-agent path. + +## Deferred from the original design + +The first version of this document proposed the following. The shipped +configuration (LCORE-3389) does not provide them; each is deferred until a +product need justifies it. + +- A dedicated `guardrails:` configuration section with separate `detectors` + and `rules`, a `src/guardrails/` package, a `DetectorBackend` protocol and + a structured `ScreeningItem` payload. +- The `openai_moderations` backend (any `/v1/moderations` service, TrustyAI + gateways) and a `llama_stack_shields` transitional backend. +- Selecting out-of-the-box Granite Guardian risk ids; risks are custom + criteria text only. +- A boolean verdict when no threshold is set; every risk has a threshold. +- `on_detector_error: allow` (fail-open) and a refusal-shaped response for + detector failures. +- `api_key_path` (reading the key from a file); the shipped config takes the + key as a secret string. +- An input execution mode that runs the guardian concurrently with the main + LLM call (former R4d). +- A global `violation_message` default; each risk carries its own. ## Open Questions for Future Work -- Request-level rule narrowing (a `guardrail_ids` analog of `shield_ids`) - — deferred from spike Decision T1; wait for a product ask. -- Unifying question-validity and PII redaction under the same - policy/config umbrella — deferred from spike Decision T7. -- Streaming checkpoint sizing defaults — spike Decision T4 (70% - confidence); tune during implementation with real latency data. -- Cheap classifier tier for `tool_content` (Prompt Guard 2-class) and its - licensing posture — deferred from spike Decisions S2/S3. -- Deprecation timeline for the OGX shields path — owned by - LCORE-1099 (spike Decision S5). +- **Model selection:** the shield targets `granite-guardian-4.1-8b` and its + 4.1 judge prompt. The spike benchmarked 3.3-8B (spike Decision S3), which + uses a different prompt format; supporting it, or serving 4.1 under a + different model name, needs a `model` setting and possibly a + version-specific prompt. +- **Guardian token usage:** whether guardian calls should count against user + quota or be tracked as service overhead. Compaction's summarization calls + raise the same question. +- **Streaming checkpoint sizing:** defaults for LCORE-3391 (spike Decision + T4, 70% confidence); tune with real latency data. +- **Cheap classifier tier for `tool`:** Prompt Guard 2-class, and its + licensing posture (spike Decisions S2 and S3). +- **Per-risk request narrowing:** `shield_ids` selects shields, not + individual risks; wait for a product ask. ## Changelog @@ -421,3 +448,4 @@ attaching the section. | 2026-08-03 | Added R4a (per-rule thresholds), R4b (per-rule violation messages), R4d (input execution mode), R7a (output-relevance context pairing); `ScreeningItem` detector payload; client-lifecycle and `src/runners` integration notes | Decisions T8–T10 and PoC finding B | | 2026-08-03 | Added R4c (recommended rule sets validated against a legitimate-question corpus) | PoC finding D — OOTB `jailbreak` false-positives on legitimate OpenShift questions at ~0.98 | | 2026-08-03 | PR #2182 review: `DetectorBackend` takes a structured payload; recommended-model rec split (3.3-8B benchmarked, 4.1-8B extrapolated) | @sbunciak / @tisnik review + CodeRabbit | +| 2026-09-10 | Architecture rewritten to the shield-based design that shipped (Granite Guardian as a `shields:` entry with `RiskDefinition`s); added R4e (advisory risks, LCORE-3391); R6 extended to topic-summary calls and RAG documents; open requirements linked to LCORE-3390, 3391, 4089 and 4090; original-design capabilities moved to "Deferred from the original design" | LCORE-3389 shipped as a shield type (PR #2580); implementation review of LCORE-3390 (PR #2646) | From 138b578f750616858596e07d6baf58af3b12d293 Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Thu, 10 Sep 2026 17:43:08 +0200 Subject: [PATCH 106/120] LCORE-3386: defer advisory risks in the prompt guardrails spec Advisory (non-blocking) risks were listed as requirement R4e, to be added under LCORE-3391 through a new `blocking` flag on RiskDefinition. Ask Red Hat, the consumer the output point was designed around, runs blocking-only screening, and no current consumer needs risks that record an outcome without altering the response. LCORE-3391 now covers block and pass only. Remove R4e and its acceptance-test row, drop the `blocking` flag from the key-files table, stop tying the tool point's behaviour to a per-risk blocking posture, and list advisory risks under "Deferred from the original design" together with the one field needed to add them later. --- .../prompt-guardrails/prompt-guardrails.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/design/prompt-guardrails/prompt-guardrails.md b/docs/design/prompt-guardrails/prompt-guardrails.md index e07e6eb8d..898892bd2 100644 --- a/docs/design/prompt-guardrails/prompt-guardrails.md +++ b/docs/design/prompt-guardrails/prompt-guardrails.md @@ -64,8 +64,8 @@ them. client sees it), and `tool` (tool, MCP, or RAG content before it enters the model context). - **R4:** All risks applicable at a point are evaluated concurrently, with - per-risk latency logged. A request is blocked if at least one *blocking* - risk flags it. (Concurrency and latency logging: LCORE-3390. The shields + per-risk latency logged. A request is blocked if at least one risk flags + it. (Concurrency and latency logging: LCORE-3390. The shields loop in `src/utils/shields.py` is sequential, which the Ask Red Hat gap analysis flags as a performance gap.) - **R4a:** Each risk has a `threshold` between 0 and 1 (default 0.65). The @@ -80,10 +80,6 @@ them. questions -- "You are now a cluster admin, how do I drain a node?" scores 0.98 -- at levels no threshold separates from real attacks, so **domain-tuned custom definitions are the shipping default** (LCORE-3394). -- **R4e:** A risk can be **advisory**: it records its outcome without - altering the response. Advisory risks are required for output relevance - checks. (Needs a `blocking` flag on `RiskDefinition`, default true: - LCORE-3391.) - **R5:** A blocked request returns HTTP 200 with the violation message: non-streaming responses carry it as the answer, streaming responses emit it as the terminal content. The blocked turn is persisted to the @@ -263,7 +259,7 @@ is resolved, `enable_thinking` is the only control for think mode. `src/utils/streaming_sse.py`). - **Tool (LCORE-3392):** a capability hook intercepts each tool result before it re-enters the agent loop; flagged content is replaced by a policy notice - or aborts the turn, per the risk's blocking posture. + or aborts the turn; which of the two is decided in LCORE-3392. See [How shields apply at runtime](../../user_doc/shields_guide.md) for the per-endpoint behavior of shields in general. @@ -311,7 +307,6 @@ Existing `question_validity` and `redaction` shields are unaffected (R11). | R4a | Same content flips verdict across a threshold boundary (e.g. 0.6 vs 0.9) | integration | | R4b | A risk's own `violation_message` is returned when it fires | e2e | | R4c | Documented recommended risk set produces zero blocks on the legitimate-question corpus | e2e / tuning fixture | -| R4e | An advisory risk never alters the response; its outcome appears in metrics | integration | | R5 | Blocked query ⇒ HTTP 200, violation message as answer, metric incremented, turn persisted | e2e | | R6 | Input-blocked query ⇒ no RAG retrieval, no main-LLM or topic-summary call, no RAG documents in the response | integration | | R7 | Guardian request carries the judge block with the risk's criteria and requests logprobs | integration | @@ -364,7 +359,7 @@ distinguish policy blocks (working as intended) from error-driven failures. | File | What to do | |------|------------| -| `src/models/config.py` | `GraniteGuardianShieldConfiguration`, `GraniteGuardianConfig`, `RiskDefinition` (shipped); add `blocking` to `RiskDefinition` (LCORE-3391) | +| `src/models/config.py` | `GraniteGuardianShieldConfiguration`, `GraniteGuardianConfig`, `RiskDefinition` (shipped) | | `src/pydantic_ai_lightspeed/capabilities/granite_guardian/` | The shield: risk selection, judge prompt, logprob scoring, `run()` and capability hooks | | `src/utils/shields.py` | `run_shield_moderation_v2` and `build_shield`; concurrent risk evaluation | | `src/app/endpoints/query.py`, `streaming_query.py` | Input shields before RAG via `run_shield_moderation_v2` (LCORE-4090) | @@ -422,6 +417,11 @@ product need justifies it. - An input execution mode that runs the guardian concurrently with the main LLM call (former R4d). - A global `violation_message` default; each risk carries its own. +- Advisory (non-blocking) risks that record their outcome without altering + the response, originally intended for output relevance checks. Ask Red Hat + runs blocking-only screening and no current consumer needs advisory risks; + a `blocking` flag on `RiskDefinition` (default true) is enough to add them + when one does. ## Open Questions for Future Work @@ -448,4 +448,4 @@ product need justifies it. | 2026-08-03 | Added R4a (per-rule thresholds), R4b (per-rule violation messages), R4d (input execution mode), R7a (output-relevance context pairing); `ScreeningItem` detector payload; client-lifecycle and `src/runners` integration notes | Decisions T8–T10 and PoC finding B | | 2026-08-03 | Added R4c (recommended rule sets validated against a legitimate-question corpus) | PoC finding D — OOTB `jailbreak` false-positives on legitimate OpenShift questions at ~0.98 | | 2026-08-03 | PR #2182 review: `DetectorBackend` takes a structured payload; recommended-model rec split (3.3-8B benchmarked, 4.1-8B extrapolated) | @sbunciak / @tisnik review + CodeRabbit | -| 2026-09-10 | Architecture rewritten to the shield-based design that shipped (Granite Guardian as a `shields:` entry with `RiskDefinition`s); added R4e (advisory risks, LCORE-3391); R6 extended to topic-summary calls and RAG documents; open requirements linked to LCORE-3390, 3391, 4089 and 4090; original-design capabilities moved to "Deferred from the original design" | LCORE-3389 shipped as a shield type (PR #2580); implementation review of LCORE-3390 (PR #2646) | +| 2026-09-10 | Architecture rewritten to the shield-based design that shipped (Granite Guardian as a `shields:` entry with `RiskDefinition`s); R6 extended to topic-summary calls and RAG documents; open requirements linked to LCORE-3390, 3391, 4089 and 4090; original-design capabilities, including advisory risks, moved to "Deferred from the original design" | LCORE-3389 shipped as a shield type (PR #2580); implementation review of LCORE-3390 (PR #2646) | From f4b90f44d0e23c92b275af927260f20c1645e7c1 Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Fri, 11 Sep 2026 14:58:41 +0200 Subject: [PATCH 107/120] LCORE-3386: tighten verify_ssl wording and fix the on-call heading in the guardrails spec Two review nits on the spec realignment: - The configuration example now states that verify_ssl must not be false when api_key is set. The Granite Guardian client sends the key as a bearer token, so disabling certificate validation on a credentialed endpoint would let an on-path attacker capture it. The constraint is documented here so the client implementation enforces it. - "Runbook / oncall implications" is spelled "on-call". --- docs/design/prompt-guardrails/prompt-guardrails.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/design/prompt-guardrails/prompt-guardrails.md b/docs/design/prompt-guardrails/prompt-guardrails.md index 898892bd2..b81c2cbeb 100644 --- a/docs/design/prompt-guardrails/prompt-guardrails.md +++ b/docs/design/prompt-guardrails/prompt-guardrails.md @@ -180,7 +180,7 @@ shields: api_key: ${env.GUARDIAN_API_KEY} # optional; requires an https URL timeout: 30 # seconds, 5-300 max_retries: 2 # 0-5 - verify_ssl: true # true | false | path to CA bundle + verify_ssl: true # true | false | path to CA bundle; must not be false when api_key is set risks: - name: roleplay-jailbreak description: >- @@ -346,7 +346,7 @@ events are page-worthy. - Configuration drift (for example an unknown point name) ⇒ startup validation error. -### Runbook / oncall implications +### Runbook / on-call implications New alert: guardian error rate (fail-closed requests). Recovery: restore the guardian endpoint, or remove or disable the affected risks (`enabled: From 6515a4b04eda66b382ae25699614aff8e5a21303 Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Fri, 11 Sep 2026 16:34:49 +0200 Subject: [PATCH 108/120] LCORE-3386: align guardrails spec with the approved LCORE-3390 implementation The approved implementation of the input guardrail point (PR #2646) changed three things the spec still described as gaps or open questions: - GraniteGuardianConfig gained a `model` field (default ibm-granite/granite-guardian-4.1-8b), so the model name sent to the inference server is configurable. The judge prompt remains built for the 4.1 format, so the open question narrows to supporting other Guardian versions, which need a version-specific prompt. - Risks at a point are checked in parallel batches of `batch_size` (default 3, 1-10) rather than one at a time, and the remaining batches are skipped once a batch flags. Batching was chosen over unbounded parallelism because internal guardian gateways rate-limit. Per-risk latency is logged. - The shield caches its model and HTTP client per configuration, so the client is created once and reused instead of per request. Update R4 and R10, the configuration example and field list, the shield section (model, concurrency, client lifecycle), the latency discussion, the acceptance-test row and test pattern for concurrency, the key-files table and the open questions, and add a changelog row. --- .../prompt-guardrails/prompt-guardrails.md | 69 +++++++++++-------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/docs/design/prompt-guardrails/prompt-guardrails.md b/docs/design/prompt-guardrails/prompt-guardrails.md index b81c2cbeb..c68c0cb77 100644 --- a/docs/design/prompt-guardrails/prompt-guardrails.md +++ b/docs/design/prompt-guardrails/prompt-guardrails.md @@ -63,11 +63,11 @@ them. prompt before the LLM call), `output` (the generated answer before the client sees it), and `tool` (tool, MCP, or RAG content before it enters the model context). -- **R4:** All risks applicable at a point are evaluated concurrently, with - per-risk latency logged. A request is blocked if at least one risk flags - it. (Concurrency and latency logging: LCORE-3390. The shields - loop in `src/utils/shields.py` is sequential, which the Ask Red Hat gap - analysis flags as a performance gap.) +- **R4:** Risks applicable at a point are evaluated concurrently, in batches + of `batch_size` (default 3) so that guardian endpoints with rate limits are + not overloaded, and per-risk latency is logged. A request is blocked if at + least one risk flags it; once a batch contains a flagged risk, the remaining + batches are skipped. - **R4a:** Each risk has a `threshold` between 0 and 1 (default 0.65). The verdict is decided by Granite Guardian's confidence score, derived from the logprobs of its verdict token, which reproduces Ask Red Hat's per-risk @@ -102,7 +102,8 @@ them. (LCORE-3391). - **R9:** Guardian errors (unreachable endpoint, timeout, unparseable verdict) fail closed: the request is not served. -- **R10:** Per-risk outcomes and latencies are logged and exposed as metrics +- **R10:** Per-risk outcomes and latencies are logged and exposed as metrics. + Per-risk latency is currently logged only; metrics are not yet exposed (LCORE-3390 for input, LCORE-3391 for output). - **R11:** The other shields (`question_validity`, `redaction`) continue to work unchanged, and may be configured alongside `granite_guardian`. @@ -177,10 +178,12 @@ shields: provider_id: granite_guardian config: url: https://guardian.example:8000/v1 + model: ibm-granite/granite-guardian-4.1-8b # default; the prompt uses the 4.1 format api_key: ${env.GUARDIAN_API_KEY} # optional; requires an https URL timeout: 30 # seconds, 5-300 max_retries: 2 # 0-5 verify_ssl: true # true | false | path to CA bundle; must not be false when api_key is set + batch_size: 3 # risks checked in parallel per batch, 1-10 risks: - name: roleplay-jailbreak description: >- @@ -205,8 +208,9 @@ Models, all extending `ConfigurationBase` (`extra="forbid"`), in "granite_guardian"`, `config`. One member of the `ShieldConfiguration` discriminated union on `provider_id`, alongside question validity and redaction. -- `GraniteGuardianConfig`: `url`, `api_key` (secret, optional), `timeout`, - `max_retries`, `verify_ssl`, `risks`. +- `GraniteGuardianConfig`: `url`, `model` (default + `ibm-granite/granite-guardian-4.1-8b`), `api_key` (secret, optional), + `timeout`, `max_retries`, `verify_ssl`, `batch_size` (default 3), `risks`. - `RiskDefinition`: `name`, `description`, `threshold` (default 0.65), `enabled` (default true), `enable_thinking` (default false), `points` (non-empty subset of `input`, `output`, `tool`), `violation_message`. @@ -228,16 +232,21 @@ is resolved, `enable_thinking` is the only control for think mode. verdict token, and computes `p_risky = p(yes) / (p(yes) + p(no))`. The risk is flagged when `p_risky >= threshold`. A response without logprobs or without a parseable verdict raises an error, which is handled per R9. -- **Model:** the implementation targets `ibm-granite/granite-guardian-4.1-8b` - and does not currently expose the model name as configuration (see Open +- **Model:** `model` sets the model name sent to the inference server + (default `ibm-granite/granite-guardian-4.1-8b`), for example to match an + Ollama tag. The judge prompt is built for the Granite Guardian 4.1 format, + so other Guardian versions need a version-specific prompt (see Open Questions). -- **Client lifecycle:** the shield should hold **one long-lived HTTP client** - for the life of the process, not one per request. Constructing a client - per check creates a fresh connection pool each time -- leaking connections - if it is never closed, and forfeiting connection reuse even when it is -- - on a path that runs on every request. The first implementation constructs - the client each time the shield is built, which happens per request; this - is tracked in the LCORE-3390 review. +- **Concurrency:** the enabled risks for a point are checked in parallel + batches of `batch_size`; once a batch contains a flagged risk, the remaining + batches are skipped. Batching bounds the load on guardian endpoints that + rate-limit, such as internal model gateways. +- **Client lifecycle:** the shield holds **one long-lived HTTP client** for + the life of the process, not one per request. Constructing a client per + check creates a fresh connection pool each time -- leaking connections if + it is never closed, and forfeiting connection reuse even when it is -- on a + path that runs on every request. The implementation caches the model and + its client per configuration, so the client is created once and reused. ### Request lifecycle integration @@ -303,7 +312,7 @@ Existing `question_validity` and `redaction` shields are unaffected (R11). | R1 | No `granite_guardian` shield ⇒ responses and latency unchanged | e2e | | R2 | A custom risk definition blocks its target phrasing and passes a benign one | e2e | | R3 | A risk with `points: [output]` never fires on input, and vice versa | integration | -| R4 | Two input risks ⇒ both guardian calls observed concurrently; per-risk latency logged | integration | +| R4 | Risks at one point ⇒ guardian calls run concurrently up to `batch_size`; later batches skipped after a flag; per-risk latency logged | integration | | R4a | Same content flips verdict across a threshold boundary (e.g. 0.6 vs 0.9) | integration | | R4b | A risk's own `violation_message` is returned when it fires | e2e | | R4c | Documented recommended risk set produces zero blocks on the legitimate-question corpus | e2e / tuning fixture | @@ -320,9 +329,10 @@ Existing `question_validity` and `redaction` shields are unaffected (R11). ### Latency and Cost Each risk adds one guardian inference on its point's critical path. With -concurrent evaluation (R4) the cost per point is roughly the slowest single -check (Guardian 8B on GPU: high tens to low hundreds of ms); evaluated -sequentially it grows linearly with the number of risks. The `tool` point +batched concurrent evaluation (R4) the cost per point is roughly the slowest +check in each batch, summed over the number of batches (Guardian 8B on GPU: +high tens to low hundreds of ms per check); a larger `batch_size` lowers +latency at the cost of more simultaneous load on the guardian endpoint. The `tool` point multiplies by the number of tool calls; deployers control exposure through point bindings, and per-risk latency (R10) makes the cost observable. Guardian token usage is not counted against user quota or reported token @@ -360,8 +370,8 @@ distinguish policy blocks (working as intended) from error-driven failures. | File | What to do | |------|------------| | `src/models/config.py` | `GraniteGuardianShieldConfiguration`, `GraniteGuardianConfig`, `RiskDefinition` (shipped) | -| `src/pydantic_ai_lightspeed/capabilities/granite_guardian/` | The shield: risk selection, judge prompt, logprob scoring, `run()` and capability hooks | -| `src/utils/shields.py` | `run_shield_moderation_v2` and `build_shield`; concurrent risk evaluation | +| `src/pydantic_ai_lightspeed/capabilities/granite_guardian/` | The shield: risk selection, judge prompt, logprob scoring, batched risk checks, cached client, `run()` and capability hooks | +| `src/utils/shields.py` | `run_shield_moderation_v2` and `build_shield` | | `src/app/endpoints/query.py`, `streaming_query.py` | Input shields before RAG via `run_shield_moderation_v2` (LCORE-4090) | | `src/utils/pydantic_ai_helpers.py` | Capabilities attached to agents; keep the tool point here, move input out (LCORE-4090) | | `src/utils/agents/streaming.py`, `src/utils/streaming_sse.py` | Output checkpoints in the SSE generators (LCORE-3391) | @@ -391,8 +401,9 @@ Follow the project's configuration conventions (see - e2e needs a guardian stand-in the CI environment can run: either the mock as a service or a small real model where resources allow; decide in the step-definitions ticket (LCORE-3388) against CI constraints. -- Concurrency: assert that risks at one point are evaluated in parallel, not - in sequence, by capturing call timestamps in the mock. +- Concurrency: assert that risks at one point run in parallel within a batch + and that later batches are skipped after a flag, by capturing call + timestamps in the mock. - Failure posture: pin the fail-closed behavior on both the `run_shield_moderation_v2` path and the in-agent path. @@ -425,10 +436,9 @@ product need justifies it. ## Open Questions for Future Work -- **Model selection:** the shield targets `granite-guardian-4.1-8b` and its - 4.1 judge prompt. The spike benchmarked 3.3-8B (spike Decision S3), which - uses a different prompt format; supporting it, or serving 4.1 under a - different model name, needs a `model` setting and possibly a +- **Other Guardian versions:** `model` accepts any model name, but the judge + prompt is built for the 4.1 format. The spike benchmarked 3.3-8B (spike + Decision S3), which uses a different prompt format; supporting it needs a version-specific prompt. - **Guardian token usage:** whether guardian calls should count against user quota or be tracked as service overhead. Compaction's summarization calls @@ -449,3 +459,4 @@ product need justifies it. | 2026-08-03 | Added R4c (recommended rule sets validated against a legitimate-question corpus) | PoC finding D — OOTB `jailbreak` false-positives on legitimate OpenShift questions at ~0.98 | | 2026-08-03 | PR #2182 review: `DetectorBackend` takes a structured payload; recommended-model rec split (3.3-8B benchmarked, 4.1-8B extrapolated) | @sbunciak / @tisnik review + CodeRabbit | | 2026-09-10 | Architecture rewritten to the shield-based design that shipped (Granite Guardian as a `shields:` entry with `RiskDefinition`s); R6 extended to topic-summary calls and RAG documents; open requirements linked to LCORE-3390, 3391, 4089 and 4090; original-design capabilities, including advisory risks, moved to "Deferred from the original design" | LCORE-3389 shipped as a shield type (PR #2580); implementation review of LCORE-3390 (PR #2646) | +| 2026-09-11 | Aligned with the LCORE-3390 implementation (PR #2646): configurable `model`, risks checked in parallel batches (`batch_size`), one cached client per configuration, per-risk latency logged | Implementation review of LCORE-3390 | From 375dbcd7bc665e542804dbdc709c571ffe4998aa Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Tue, 15 Sep 2026 14:38:02 +0200 Subject: [PATCH 109/120] LCORE-3386: use the OGX name for the deferred shields bridge backend The "Deferred from the original design" section listed the transitional backend as `llama_stack_shields`. The docs-wide OGX naming update on main (a2aae407) renamed that backend to `ogx_shields` in the original design text, and no other Llama Stack name is left in this spec, so the deferred list now uses the same name. --- docs/design/prompt-guardrails/prompt-guardrails.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design/prompt-guardrails/prompt-guardrails.md b/docs/design/prompt-guardrails/prompt-guardrails.md index c68c0cb77..3cfee9181 100644 --- a/docs/design/prompt-guardrails/prompt-guardrails.md +++ b/docs/design/prompt-guardrails/prompt-guardrails.md @@ -417,7 +417,7 @@ product need justifies it. and `rules`, a `src/guardrails/` package, a `DetectorBackend` protocol and a structured `ScreeningItem` payload. - The `openai_moderations` backend (any `/v1/moderations` service, TrustyAI - gateways) and a `llama_stack_shields` transitional backend. + gateways) and an `ogx_shields` transitional backend. - Selecting out-of-the-box Granite Guardian risk ids; risks are custom criteria text only. - A boolean verdict when no threshold is set; every risk has a threshold. From 148bfb6c0d38afc4006db3742980c6495da67525 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Tue, 15 Sep 2026 18:41:15 +0200 Subject: [PATCH 110/120] LCORE-2441: Updated hexagonal architecture --- docs/devel_doc/hexagonal_architecture.svg | 586 +++++++++++++++------- 1 file changed, 404 insertions(+), 182 deletions(-) diff --git a/docs/devel_doc/hexagonal_architecture.svg b/docs/devel_doc/hexagonal_architecture.svg index 13ed68baf..eaadbd9c5 100644 --- a/docs/devel_doc/hexagonal_architecture.svg +++ b/docs/devel_doc/hexagonal_architecture.svg @@ -2,9 +2,9 @@ + id="path5" /> + gradientTransform="matrix(0.16337138,0,0,-0.16337138,133.25422,148.00939)"> + gradientTransform="matrix(0.16337138,0,0,-0.16337138,133.25422,148.00939)"> + transform="translate(50.054891,39.196608)" /> + transform="matrix(0.56185068,0,0,0.56185068,94.049311,76.128406)" /> + x="129.87538" + y="114.5075" /> Service + x="135.41661" + y="127.37009">Service Domain + x="147.10591" + y="97.554108">Domain + x="92.497955" + y="102.7272" /> Port + x="96.142654" + y="110.60452">Port + x="182.55051" + y="103.21798" /> Port + x="186.19521" + y="111.09533">Port Port + x="61.107365" + y="145.67564">Port Port + x="182.48856" + y="-4.9579186">Port Port + x="182.2867" + y="75.031456">Port Port + x="60.615051" + y="224.95256">Port + x="49.279972" + y="101.94353" /> Adapter + x="50.669312" + y="109.91415">Adapter + x="214.41187" + y="102.43433" /> Adapter + x="215.80121" + y="110.40491">Adapter + x="182.55051" + y="119.7192" /> Port + x="186.19521" + y="127.5966">Port + x="214.41187" + y="118.93554" /> Adapter + x="215.80121" + y="126.9062">Adapter Adapter + x="51.649933" + y="113.64037">Adapter Adapter + x="177.36275" + y="-35.432091">Adapter Adapter + x="54.669758" + y="253.36449">Adapter Adapter + x="171.65491" + y="105.52328">Adapter Request flow + x="46.856876" + y="36.861885">Request flow REST API Database LLMsLLMs Telemetry + x="0.91574913" + y="109.02024">Telemetry Metrics + x="2.0523758" + y="126.99681">Metrics OKP MCP + x="271" + y="110">OKP MCP BYOK + x="271.16904" + y="126.88011">BYOK Agents + x="192.0298" + y="194.25768">Agents + x="92.497955" + y="119.23096" /> Port + x="96.142654" + y="127.10838">Port + x="49.279972" + y="118.4473" /> Adapter + x="50.669315" + y="126.41798">Adapter Primary actors + x="46.856876" + y="19.557201">Primary actors Secondary actors + x="186.10043" + y="19.557201">Secondary actors + style="fill:none;stroke:#000000;stroke-width:0.697;stroke-dasharray:none;stroke-opacity:1" + d="m 237.68603,43.766986 a 7.1999664,3.1965318 0 0 0 -7.20007,3.196187 7.1999664,3.1965318 0 0 0 0.002,0.06356 v 10.47998 a 7.1999636,3.196532 0 0 0 -0.002,0.06666 7.1999636,3.196532 0 0 0 0.002,0.06666 v 0.128157 h 0.0119 a 7.1999636,3.196532 0 0 0 7.18664,3.001884 7.1999636,3.196532 0 0 0 7.18665,-3.001884 h 0.0119 v -0.128157 a 7.1999636,3.196532 0 0 0 0.002,-0.06666 7.1999636,3.196532 0 0 0 -0.002,-0.06666 V 47.027768 a 7.1999664,3.1965318 0 0 0 0.002,-0.06459 7.1999664,3.1965318 0 0 0 -7.20008,-3.196187 z" /> + style="stroke-width:0.952565" /> + transform="matrix(0.26458333,0,0,0.26458333,126.29999,205.87184)"> @@ -1103,7 +1103,7 @@ + + + + + + + + + Feedback + + + + + + + + + + Transcripts + Port + Adapter + + + + + + + + + + + + + + + + + + + + + Date: Tue, 15 Sep 2026 15:05:38 -0400 Subject: [PATCH 111/120] fix(mcp): mcp auth header, "bearer" duplication --- src/utils/mcp/mcp_headers.py | 44 ++++++++++++ src/utils/mcp/mcp_oauth_probe.py | 13 ++-- src/utils/mcp/mcp_tools.py | 9 +-- src/utils/responses.py | 12 ++++ tests/unit/utils/mcp/test_mcp_headers.py | 45 +++++++++++++ tests/unit/utils/test_responses.py | 85 +++++++++++++++++++++++- 6 files changed, 190 insertions(+), 18 deletions(-) diff --git a/src/utils/mcp/mcp_headers.py b/src/utils/mcp/mcp_headers.py index 980d7a421..f3473884f 100644 --- a/src/utils/mcp/mcp_headers.py +++ b/src/utils/mcp/mcp_headers.py @@ -129,6 +129,50 @@ def extract_propagated_headers( return propagated +def strip_bearer_prefix(value: str) -> str: + """Strip a leading ``Bearer`` scheme from an Authorization header value. + + OGX's MCP tool ``authorization`` field expects a raw token/credential and + unconditionally prepends its own ``Bearer `` prefix before forwarding the + request to the downstream MCP server. Values built by this module (e.g. + the resolved Kubernetes token, or an ``Authorization`` header propagated + verbatim from the incoming request) already include the ``Bearer`` scheme + since that's the correct on-the-wire header representation. Passing such + a value straight through to OGX's ``authorization`` field would result in + a duplicated ``Bearer Bearer `` header at the downstream MCP + server, so callers that populate that field must strip the scheme first. + + Args: + value: The raw Authorization header value, with or without a + ``Bearer`` scheme. + + Returns: + The value with a leading ``Bearer`` scheme (exact casing) and its + separating whitespace removed. Values without the scheme, including + those with a differently-cased scheme (e.g. ``bearer``), are + returned unchanged. + """ + return value.removeprefix("Bearer ") + + +def ensure_bearer_prefix(value: str) -> str: + """Ensure an Authorization header value carries the ``Bearer`` scheme. + + Idempotent counterpart to :func:`strip_bearer_prefix`: a value that + already has the exact ``Bearer`` scheme is not double-prefixed, while a + raw token (or a value with a differently-cased scheme, e.g. ``bearer``, + which is treated as a raw token) gets the ``Bearer`` scheme added. + + Args: + value: A raw token, or a value that may already start with the + exact ``Bearer`` scheme. + + Returns: + The value guaranteed to start with a single ``Bearer `` prefix. + """ + return f"Bearer {strip_bearer_prefix(value)}" + + def find_unresolved_auth_headers( configured: Mapping[str, str], resolved: Mapping[str, str], diff --git a/src/utils/mcp/mcp_oauth_probe.py b/src/utils/mcp/mcp_oauth_probe.py index 17c3d265d..6fad35153 100644 --- a/src/utils/mcp/mcp_oauth_probe.py +++ b/src/utils/mcp/mcp_oauth_probe.py @@ -15,7 +15,7 @@ from configuration import AppConfig from log import get_logger from models.api.responses.error import UnauthorizedResponse -from utils.mcp.mcp_headers import McpHeaders, build_mcp_headers +from utils.mcp.mcp_headers import McpHeaders, build_mcp_headers, ensure_bearer_prefix logger = get_logger(__name__) @@ -57,14 +57,9 @@ async def check_mcp_auth( for mcp_server in configuration.mcp_servers: headers = complete_headers.get(mcp_server.name, {}) auth_header = headers.get("Authorization") - if auth_header is not None: - authorization = ( - auth_header - if auth_header.startswith("Bearer ") - else f"Bearer {auth_header}" - ) - else: - authorization = None + authorization = ( + ensure_bearer_prefix(auth_header) if auth_header is not None else None + ) if ( authorization diff --git a/src/utils/mcp/mcp_tools.py b/src/utils/mcp/mcp_tools.py index 8f30699de..997eb9c32 100644 --- a/src/utils/mcp/mcp_tools.py +++ b/src/utils/mcp/mcp_tools.py @@ -11,6 +11,7 @@ from log import get_logger from models.common.tools import ListedMcpTool +from utils.mcp.mcp_headers import ensure_bearer_prefix logger = get_logger(__name__) @@ -106,12 +107,8 @@ def _prepare_mcp_request_headers(headers: dict[str, str]) -> dict[str, str]: """ prepared = dict(headers) for header_name, value in list(prepared.items()): - if ( - header_name.lower() == "authorization" - and value - and not value.startswith("Bearer ") - ): - prepared[header_name] = f"Bearer {value}" + if header_name.lower() == "authorization" and value: + prepared[header_name] = ensure_bearer_prefix(value) return prepared diff --git a/src/utils/responses.py b/src/utils/responses.py index 7f856772d..cbb7e83e6 100644 --- a/src/utils/responses.py +++ b/src/utils/responses.py @@ -114,6 +114,7 @@ McpHeaders, build_mcp_headers, find_unresolved_auth_headers, + strip_bearer_prefix, ) from utils.model_list import parse_model_list_response from utils.otel_tracing import ( @@ -777,6 +778,13 @@ async def get_mcp_tools( continue authorization = headers.pop("Authorization", None) + if authorization is not None: + # OGX's "authorization" field expects a raw token/credential and + # prepends its own "Bearer " scheme downstream; strip any scheme + # already present (e.g. from Kubernetes auth or a propagated + # request header) to avoid a duplicated "Bearer Bearer " + # header at the MCP server. + authorization = strip_bearer_prefix(authorization) require_approval = ( mcp_server.require_approval @@ -856,6 +864,10 @@ def apply_mcp_headers_to_explicit_tools( continue authorization = headers.pop("Authorization", None) + if authorization is not None: + # See the equivalent comment in get_mcp_tools: OGX prepends its + # own "Bearer " scheme, so strip any scheme already present here. + authorization = strip_bearer_prefix(authorization) out.append( mcp_tool.model_copy( update={ diff --git a/tests/unit/utils/mcp/test_mcp_headers.py b/tests/unit/utils/mcp/test_mcp_headers.py index 8bc4b4449..70dac527f 100644 --- a/tests/unit/utils/mcp/test_mcp_headers.py +++ b/tests/unit/utils/mcp/test_mcp_headers.py @@ -11,8 +11,10 @@ from utils.mcp import mcp_headers from utils.mcp.mcp_headers import ( build_server_headers, + ensure_bearer_prefix, extract_propagated_headers, find_unresolved_auth_headers, + strip_bearer_prefix, ) @@ -335,6 +337,49 @@ def test_empty_resolved_returns_all_configured(self) -> None: assert sorted(result) == ["Authorization", "X-Api-Key"] +class TestStripBearerPrefix: + """Test cases for strip_bearer_prefix function.""" + + def test_strips_bearer_prefix(self) -> None: + """Test that a leading 'Bearer ' scheme is removed.""" + assert strip_bearer_prefix("Bearer abc123") == "abc123" + + def test_does_not_strip_differently_cased_prefix(self) -> None: + """Test that only the exact 'Bearer ' scheme is matched, not other casings.""" + assert strip_bearer_prefix("bearer abc123") == "bearer abc123" + assert strip_bearer_prefix("BEARER abc123") == "BEARER abc123" + assert strip_bearer_prefix("BeArEr abc123") == "BeArEr abc123" + + def test_value_without_prefix_is_unchanged(self) -> None: + """Test that a raw token without a scheme is returned as-is.""" + assert strip_bearer_prefix("abc123") == "abc123" + + def test_empty_string_is_unchanged(self) -> None: + """Test that an empty string is returned unchanged.""" + assert not strip_bearer_prefix("") + + def test_only_strips_leading_prefix(self) -> None: + """Test that only the first 'Bearer ' scheme is stripped, not repeated ones.""" + assert strip_bearer_prefix("Bearer Bearer abc123") == "Bearer abc123" + + +class TestEnsureBearerPrefix: + """Test cases for ensure_bearer_prefix function.""" + + def test_adds_bearer_prefix_to_raw_token(self) -> None: + """Test that a raw token gets the 'Bearer ' scheme added.""" + assert ensure_bearer_prefix("abc123") == "Bearer abc123" + + def test_does_not_double_prefix_already_scoped_value(self) -> None: + """Test that a value already carrying 'Bearer ' is left unchanged.""" + assert ensure_bearer_prefix("Bearer abc123") == "Bearer abc123" + + def test_double_prefixes_differently_cased_scheme(self) -> None: + """Test that a differently-cased scheme is treated as a raw token and prefixed.""" + assert ensure_bearer_prefix("bearer abc123") == "Bearer bearer abc123" + assert ensure_bearer_prefix("BEARER abc123") == "Bearer BEARER abc123" + + class TestBuildServerHeaders: """Test cases for build_server_headers function.""" diff --git a/tests/unit/utils/test_responses.py b/tests/unit/utils/test_responses.py index ae9b98dac..791019b96 100644 --- a/tests/unit/utils/test_responses.py +++ b/tests/unit/utils/test_responses.py @@ -482,7 +482,10 @@ async def test_get_mcp_tools_with_kubernetes_auth( mocker.patch("utils.responses.configuration", mock_config) tools_k8s = await get_mcp_tools(token="user-k8s-token") assert len(tools_k8s) == 1 - assert tools_k8s[0].authorization == "Bearer user-k8s-token" + # The Bearer scheme is stripped here since OGX adds its own "Bearer " + # prefix before forwarding the token to the MCP server; passing the + # scheme through would result in a duplicated "Bearer Bearer ". + assert tools_k8s[0].authorization == "user-k8s-token" @pytest.mark.asyncio async def test_get_mcp_tools_with_mcp_headers(self, mocker: MockerFixture) -> None: @@ -620,7 +623,7 @@ async def test_get_mcp_tools_with_mixed_headers( tools = await get_mcp_tools(token="k8s-token", mcp_headers=mcp_headers) assert len(tools) == 1 - assert tools[0].authorization == "Bearer k8s-token" + assert tools[0].authorization == "k8s-token" assert tools[0].headers == { "X-API-Key": "secret-api-key", "X-Custom": "client-custom-value", @@ -705,6 +708,40 @@ async def test_get_mcp_tools_with_propagated_headers( "x-request-id": "req-456", } + @pytest.mark.asyncio + async def test_get_mcp_tools_propagated_authorization_strips_bearer_scheme( + self, mocker: MockerFixture + ) -> None: + """Regression test for a duplicated 'Bearer Bearer ' MCP header. + + When an MCP server allowlists the incoming ``Authorization`` header + via ``headers: [Authorization]`` (no ``authorization_headers`` + config), the propagated value already includes the ``Bearer`` + scheme. Since OGX's MCP ``authorization`` field expects a raw token + and adds its own ``Bearer `` prefix, the scheme must be stripped + before it is assigned to that field, or the downstream MCP server + receives ``Authorization: Bearer Bearer ``. + """ + servers = [ + ModelContextProtocolServer( + name="subscription-watch", + url="http://subscription-watch:8080", + headers=["Authorization"], + provider_id="provider", + ), + ] + mock_config = mocker.Mock() + mock_config.mcp_servers = servers + mocker.patch("utils.responses.configuration", mock_config) + + request_headers = {"Authorization": "Bearer user-jwt-token"} + tools = await get_mcp_tools( + token=None, mcp_headers=None, request_headers=request_headers + ) + assert len(tools) == 1 + assert tools[0].authorization == "user-jwt-token" + assert tools[0].headers is None + @pytest.mark.asyncio async def test_get_mcp_tools_propagated_headers_do_not_overwrite_auth_headers( self, tmp_path: Path, mocker: MockerFixture @@ -812,7 +849,7 @@ async def test_get_mcp_tools_propagated_headers_additive_with_mcp_headers( token=None, mcp_headers=mcp_hdrs, request_headers=request_headers ) assert len(tools) == 1 - assert tools[0].authorization == "Bearer client-token" + assert tools[0].authorization == "client-token" assert tools[0].headers == {"x-rh-identity": "identity-value"} @pytest.mark.asyncio @@ -931,6 +968,48 @@ async def test_apply_mcp_headers_preserves_type( dumped = out[0].model_dump(exclude_unset=True) assert dumped.get("type") == "mcp" + def test_apply_mcp_headers_strips_bearer_from_propagated_authorization( + self, mocker: MockerFixture + ) -> None: + """apply_mcp_headers_to_explicit_tools must strip the Bearer scheme. + + Same regression as ``test_get_mcp_tools_propagated_authorization_strips_bearer_scheme`` + but for the explicit-tools path: a propagated ``Authorization`` header + must not retain its ``Bearer`` scheme when assigned to OGX's + ``authorization`` field, or the MCP server receives a duplicated + ``Bearer Bearer `` header. + """ + from utils.responses import ( # pylint: disable=import-outside-toplevel + apply_mcp_headers_to_explicit_tools, + ) + + servers = [ + ModelContextProtocolServer( + name="subscription-watch", + url="http://subscription-watch:8080", + headers=["Authorization"], + provider_id="mcp", + ), + ] + mock_config = mocker.Mock() + mock_config.mcp_servers = servers + mocker.patch("utils.responses.configuration", mock_config) + + explicit = InputToolMCP( + server_label="subscription-watch", + server_url="http://subscription-watch:8080", + ) + + out = apply_mcp_headers_to_explicit_tools( + [explicit], + token=None, + mcp_headers=None, + request_headers={"Authorization": "Bearer user-jwt-token"}, + ) + + assert len(out) == 1 + assert out[0].authorization == "user-jwt-token" + class TestGetTopicSummary: """Tests for get_topic_summary function.""" From 39a62d927860a9bf6462b5f91e8bb336c1445e87 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 15 Sep 2026 16:35:27 -0400 Subject: [PATCH 112/120] adding pgvector in pyproject.toml for OGX Signed-off-by: Lucas --- pyproject.toml | 1 + uv.lock | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 02cf39296..74f971d07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -178,6 +178,7 @@ ogxlibdev = [ "faiss-cpu>=1.11.0", "chardet>=5.2.0", "psycopg2-binary>=2.9.10", + "pgvector>=0.3.6", "pypdf>=6.10.2", # API scoring: inline::basic "requests>=2.33.0", diff --git a/uv.lock b/uv.lock index eb54f1900..6cf2be2a8 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.12, <3.15" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", @@ -2290,6 +2290,7 @@ ogxlibdev = [ { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-sdk" }, { name = "peft" }, + { name = "pgvector" }, { name = "protobuf" }, { name = "psutil" }, { name = "psycopg2-binary" }, @@ -2406,6 +2407,7 @@ ogxlibdev = [ { name = "opentelemetry-instrumentation", specifier = ">=0.55b0" }, { name = "opentelemetry-sdk", specifier = ">=1.34.1" }, { name = "peft", specifier = ">=0.15.2" }, + { name = "pgvector", specifier = ">=0.3.6" }, { name = "protobuf", specifier = ">=6.33.5" }, { name = "psutil", specifier = ">=7.0.0" }, { name = "psycopg2-binary", specifier = ">=2.9.10" }, @@ -3459,6 +3461,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/28/79/13bcabb8048126422d5c4b880575d40886c726f354db88cfeed4325525bb/peft-0.20.0-py3-none-any.whl", hash = "sha256:0fbba16ffebfad3de96e06f2da6860fd860292324b85b6141909fa1e26ea9233", size = 775777, upload-time = "2026-07-28T13:45:59.809Z" }, ] +[[package]] +name = "pgvector" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/ec/6eb80aebc728200f95229219882994c1b0585b956ca47da5edb9d062627a/pgvector-0.5.0.tar.gz", hash = "sha256:07a9dcf735696879406983afc6eba9a787cef7c0cf6c367ca1a5779f036dee74", size = 35170, upload-time = "2026-07-06T18:27:27.767Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/e4/a5573f2c579ca9ad133293bfb624148ba0893674ca4a6eeec85ced9a6a09/pgvector-0.5.0-py3-none-any.whl", hash = "sha256:fedc9800894e6da2be51358d7b7c574bf34f247ca741a5a09513622135f5964f", size = 30958, upload-time = "2026-07-06T18:27:26.797Z" }, +] + [[package]] name = "pip" version = "26.1" From 327ffb7c3dae0026717c77407d4016616c47f33f Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Wed, 16 Sep 2026 09:07:42 +0200 Subject: [PATCH 113/120] LCORE-3599: Added missing type annotation --- tests/unit/utils/test_otel_tracing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/utils/test_otel_tracing.py b/tests/unit/utils/test_otel_tracing.py index af48fb0cb..c9d037bd2 100644 --- a/tests/unit/utils/test_otel_tracing.py +++ b/tests/unit/utils/test_otel_tracing.py @@ -215,7 +215,7 @@ def test_set_attributes_with_list(self, otel: Generator[Any, Any, Any]) -> None: ) assert attrs[SpanAttributes.TOOL_CALLS_NAMES] == ("search", "calculator") - def test_set_empty_attributes(self, otel): + def test_set_empty_attributes(self, otel: Generator[Any, Any, Any]) -> None: """Test setting empty attributes dict.""" tracer, exporter = otel with tracer.start_as_current_span("test_span") as span: From 35e34dc71ac80fe436060bce7d6dc31161e7e3bc Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Wed, 16 Sep 2026 09:09:46 +0200 Subject: [PATCH 114/120] LCORE-3991: Enable rule B011 on CI --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 02cf39296..15d5465c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -251,7 +251,7 @@ line-length = 88 extend-exclude = ["tests/profiles/syntax_error.py"] [tool.ruff.lint] -extend-select = ["TID251", "UP006", "UP007", "UP008", "UP010", "UP012", "UP017", "UP024", "UP035", "UP040", "UP041", "RUF100", "B009", "B010", "DTZ005", "D202", "I001", "PLR1733", "RUF022", "PLW1510", "TC004", "PIE790", "PERF402", "FURB129", "RET501"] +extend-select = ["TID251", "UP006", "UP007", "UP008", "UP010", "UP012", "UP017", "UP024", "UP035", "UP040", "UP041", "RUF100", "B009", "B010", "B011", "DTZ005", "D202", "I001", "PLR1733", "RUF022", "PLW1510", "TC004", "PIE790", "PERF402", "FURB129", "RET501"] ignore = ["UP047", "UP045", "BLE", "S", "C", "RUF", "SIM", "B017", "TRY004", "TRY201", "TRY203", "TRY401", "B008", "EXE001", "G201", "ISC004", "LOG014", "PYI034", "PYI064"] [tool.ruff.lint.flake8-tidy-imports.banned-api] From 05505566ec0ff3867667ca85459e604f6769cb4a Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Wed, 16 Sep 2026 09:12:53 +0200 Subject: [PATCH 115/120] LCORE-4125: Unnecessary assignment to new_response before return statement --- .../capabilities/redaction/_capability.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pydantic_ai_lightspeed/capabilities/redaction/_capability.py b/src/pydantic_ai_lightspeed/capabilities/redaction/_capability.py index 29c3ef910..15681d8b4 100644 --- a/src/pydantic_ai_lightspeed/capabilities/redaction/_capability.py +++ b/src/pydantic_ai_lightspeed/capabilities/redaction/_capability.py @@ -321,13 +321,11 @@ async def after_model_request( A new ModelResponse with redacted text parts, or the original if no redaction occurred. """ - new_response = _redact_response( + return _redact_response( response, self.config.compiled_patterns, ) - return new_response - async def run(self, input_text: str) -> ShieldModerationResult: """Run PII redaction on input text and return a moderation result.""" result = redact_text(input_text, self.config.compiled_patterns) From 64cb584814d51ee4a51cfcaa00adad935e10c318 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Wed, 16 Sep 2026 09:15:04 +0200 Subject: [PATCH 116/120] LCORE-3820: Updated dependencies --- uv.lock | 449 +++++++++++++++++++++++++++----------------------------- 1 file changed, 219 insertions(+), 230 deletions(-) diff --git a/uv.lock b/uv.lock index eb54f1900..879e5a7c3 100644 --- a/uv.lock +++ b/uv.lock @@ -214,7 +214,7 @@ wheels = [ [[package]] name = "anthropic" -version = "1.5.0" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -225,9 +225,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/43/6f3f6006f5216d43a059a1a856d275ef6536cdfa94883b64cc04d7873cb7/anthropic-1.5.0.tar.gz", hash = "sha256:b25f87f5758861f25993383a5c9bf274eb6e0f1b010c84019ad91854cb4e1bc6", size = 1156657, upload-time = "2026-09-10T17:45:35.93Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/3d/4170318539de0c163e1806509b0bb9cd7d611ee15e7885f1d16a11a37d29/anthropic-1.6.0.tar.gz", hash = "sha256:ce3c032f940984f67c516db896375bff36959f8dc589973e386f8e6ea81bbec3", size = 1172375, upload-time = "2026-09-15T15:14:55.826Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/1c/c32fce35ca0be0205f2377d2238e3ed73dea5abc2be09d15fbd032091d60/anthropic-1.5.0-py3-none-any.whl", hash = "sha256:d9ce04b29ad1f7025dda3e3bc478cde8d2924d2de5417d2d4a4bb0378ecbc2a6", size = 1235236, upload-time = "2026-09-10T17:45:34.216Z" }, + { url = "https://files.pythonhosted.org/packages/c1/32/57a8b6c5441d9e6a1f332f70ee02375af6d9b5b65b42689ba83f7c7d010a/anthropic-1.6.0-py3-none-any.whl", hash = "sha256:049434f013d3a874acdfee696ec626e19166d58f4277e8fa34e5d0a5ec250ad0", size = 1249068, upload-time = "2026-09-15T15:14:54.326Z" }, ] [[package]] @@ -514,30 +514,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.43.93" +version = "1.43.95" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/37/7a09d8320685b3b8c8a014e392e7595025d00965c46c5f410797905fab7a/boto3-1.43.93.tar.gz", hash = "sha256:196bfc8b4c9cd5505f9f7b963e30956db3a00fd47e20dd0ee3574a243c1fb212", size = 112752, upload-time = "2026-09-11T19:23:03.484Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/ed/9d4d4e7d4b874c16f7b3a886efe3e45801e0f9ebb60058adf1bb69cb2073/boto3-1.43.95.tar.gz", hash = "sha256:9d71f299111e1f4e8c28f573a1b7c0555fe40d2147fe9bef852a02bd57cbde60", size = 112666, upload-time = "2026-09-15T19:23:49.395Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/68/f8f661b9e68daba4f775bfa1e750732ec52fc89b32d55f300aef530e1d62/boto3-1.43.93-py3-none-any.whl", hash = "sha256:3c948fe231490d446bf90bf3322d1452632107329d3683b37d88b7399bf481a0", size = 140022, upload-time = "2026-09-11T19:23:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/66/6e/9c6fb58b1cdddf13b3e9edc94e0c6e08fff456355f6e097684d6117aa8d4/boto3-1.43.95-py3-none-any.whl", hash = "sha256:c906921c4f9ab41e9587af6586f072c8f2be6979e8bde2ec26aa443bb1745019", size = 140026, upload-time = "2026-09-15T19:23:47.563Z" }, ] [[package]] name = "botocore" -version = "1.43.93" +version = "1.43.95" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c8/a0/2ce10897323d67dd85de6190fdee159013a75741d1dc48b74d4815ec0592/botocore-1.43.93.tar.gz", hash = "sha256:82da355d18a7f784347b00444be33942834651f31b6c5ffef49999cd47364c5e", size = 16103276, upload-time = "2026-09-11T19:22:58.933Z" } +sdist = { url = "https://files.pythonhosted.org/packages/db/2c/0031468b521eefed325a3da2d6576e068aab95837af0c96effea6438851c/botocore-1.43.95.tar.gz", hash = "sha256:779588da32bd48a7bb0c097da4bcb747260e86d2bfc507369dca56f1450e0722", size = 16106887, upload-time = "2026-09-15T19:23:44.23Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/7e/f858c401f32d980f924c8f8328b62fab463313834a9ad2326f44613b97ec/botocore-1.43.93-py3-none-any.whl", hash = "sha256:3ca57bb5d26d88b554a74de708a5c991f45306436c91aacca931252d1d4d54ff", size = 15794156, upload-time = "2026-09-11T19:22:55.964Z" }, + { url = "https://files.pythonhosted.org/packages/e9/77/7ce7c937b31903f1402e478fdc97f98e727656fcea3cb54e74f1ca55793e/botocore-1.43.95-py3-none-any.whl", hash = "sha256:0fda26d16c7c7bf7082c421390a817b696f98a43d3da7c43dbbb093f76662876", size = 15800616, upload-time = "2026-09-15T19:23:41.228Z" }, ] [[package]] @@ -1213,11 +1213,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.32.6" +version = "3.32.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/38/46/126b1831dca12060d4a8296bf9c4fe5c93c4f22197fa239cb0cc82042bba/filelock-3.32.6.tar.gz", hash = "sha256:a3f55a18af3652a94d8f47d6055df434f254ca1d02ef2524850c6d249ca2512c", size = 225172, upload-time = "2026-09-08T22:57:11.528Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/59/e19834834cb01a32febfbb0f8a23a9088088f5d45991824ff2bc3b5e8acb/filelock-3.32.7.tar.gz", hash = "sha256:37b8a3d9811b0f9aef7e5ec5c71bb320de52df51e6ca9bcd6f5ad81187660da7", size = 225154, upload-time = "2026-09-16T00:24:20.907Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/06/4f138f618dbea66803291274f228f01daf29f306fe8b96bc30dab765df75/filelock-3.32.6-py3-none-any.whl", hash = "sha256:3f16ecd0117feae0dfc147e8c62eb5daeccd8bd800378c3ddf416de9b4feb6b1", size = 100189, upload-time = "2026-09-08T22:57:10.182Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/31098c5aeb4d966b553641472bd55fcf5fdfac953549894b8a765ba44e91/filelock-3.32.7-py3-none-any.whl", hash = "sha256:65ff0d0190ea42038b32bda4b77834fb05be2cad4c5b9b01aa4dfb3614536e52", size = 100157, upload-time = "2026-09-16T00:24:19.543Z" }, ] [[package]] @@ -1337,15 +1337,15 @@ http = [ [[package]] name = "genai-prices" -version = "0.1.6" +version = "0.1.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx2" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1a/16/e5a507d42c0eb629b48ebe6c278f2d8c3f929bb6b28f18108bdd66d8ae12/genai_prices-0.1.6.tar.gz", hash = "sha256:802c1e4cc3ed5e70a09083b83af441a58d91f62e12768f7f1b6b26c98a33fcac", size = 111810, upload-time = "2026-09-02T14:53:54.895Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/62/5f294af24a7ae407e6fa1a7ff36d310bed36b89ea6cea7eb7cfac5c305ff/genai_prices-0.1.7.tar.gz", hash = "sha256:4305395c5891796860c697cc3a49ab50b751ed62c8ff1911e291ea8da77f3a91", size = 114996, upload-time = "2026-09-15T11:24:10.852Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/a1/43fa2a4c5557cd977e83eecec265b0b77b726b0c7b9f2f180b46c6fdb458/genai_prices-0.1.6-py3-none-any.whl", hash = "sha256:35ac8043dbcf2958488129413bfecba7304fe12a68ad4a78c5b0d15281e82814", size = 118834, upload-time = "2026-09-02T14:53:53.758Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/9e04f3dca5ffe74133779ab5e9925ffb6411afbeac55d26fa0e148722599/genai_prices-0.1.7-py3-none-any.whl", hash = "sha256:090ba285549e0d2ea3187d05f091a5f0ae722e652143fa844ac809205aecd023", size = 121970, upload-time = "2026-09-15T11:24:09.54Z" }, ] [[package]] @@ -1394,7 +1394,7 @@ requests = [ [[package]] name = "google-cloud-aiplatform" -version = "2.1.0" +version = "2.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -1411,9 +1411,9 @@ dependencies = [ { name = "pydantic" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6f/cc/0c562f5d268f07234e712ee8822dd81ec4c836559f449d9e5e91e9aa2025/google_cloud_aiplatform-2.1.0.tar.gz", hash = "sha256:964eca160d4af48a2e04b5ee476fb4d38c84388f23b4420e2c71b30151d625bd", size = 11331982, upload-time = "2026-09-01T19:11:15.526Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/22/2c0f9ccb918d67e9e34ca857337670c97fe49a84ab31979060eb9112dc64/google_cloud_aiplatform-2.1.3.tar.gz", hash = "sha256:6ccd59e9bd5f313209cfe1bef8c4ab2f57ef2c91b96003dd88053df3a5757dc6", size = 11335191, upload-time = "2026-09-16T02:29:39.991Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/26/10f3d4ab6333672ff43ad56255b74e672c2a59b21b7a054632d05b7dd677/google_cloud_aiplatform-2.1.0-py2.py3-none-any.whl", hash = "sha256:de5c6dace6cb81943fc6ee1fad02f7a02e9b18e50c24b267bd9627f6b9cd3d14", size = 9452898, upload-time = "2026-09-01T19:11:08.19Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b3/be1a23d1cbc239f43de9d4a97dd5e2623a04735125aa41a214822b2e8ad6/google_cloud_aiplatform-2.1.3-py2.py3-none-any.whl", hash = "sha256:846d5dcda761832933bac34aeca1faba5d12819299d200e7a215c22c04770843", size = 9455465, upload-time = "2026-09-16T02:29:36.445Z" }, ] [[package]] @@ -2432,7 +2432,7 @@ wheels = [ [[package]] name = "litellm" -version = "1.100.1" +version = "1.101.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -2450,15 +2450,15 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/48/a5/f78d2fafa950040f833efd41dd5690598afeef0c9c4657171372573698aa/litellm-1.100.1.tar.gz", hash = "sha256:d24b5fbdeb1f0b5c0a6f7f0caf7b6aa79b69c704b90daca57e3ce7d50a9d6bf4", size = 17330008, upload-time = "2026-09-10T01:42:53.328Z" } +sdist = { url = "https://files.pythonhosted.org/packages/71/c0/08a31c8c5f7fce96e98e2a7f818f446d0468da484228a7450cd2b6f71dcf/litellm-1.101.0.tar.gz", hash = "sha256:734ab2b8cad6a3b582d52d9c9c5fcab759eb382b93935ef808fda0e16d822ac3", size = 17447699, upload-time = "2026-09-14T23:15:36.135Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/b7/04e5f938a1c3aa4db5a77287571f700e5e217199c1524ebc991933537c49/litellm-1.100.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4e2d410538c8a0d7b58440c47d7e1a6120c3219dfb5d100f39069fa8dbce0055", size = 23899191, upload-time = "2026-09-10T01:42:29.375Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b6/3105d3a3778875139bf2ba71eae0b37c83a269beca7433a3b0bef9cdd9ed/litellm-1.100.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:1d2e622a2e2eab74efd25ed356208568b783cb406f413efd083e3416b81d1606", size = 23552125, upload-time = "2026-09-10T01:42:33.277Z" }, - { url = "https://files.pythonhosted.org/packages/2d/8d/1417d7bd3708789f8b5b9334ea996461b119615cd9de4c0dce881354e095/litellm-1.100.1-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f24ab714b58a66276623b48ed58c0fbf4e1ccd6fd127b187a049ce0c74eba8ab", size = 23693099, upload-time = "2026-09-10T01:42:36.241Z" }, - { url = "https://files.pythonhosted.org/packages/1c/76/e087de4ea74635a790e2d8e65870409eae872a3d2dc9773a65b9d0559a35/litellm-1.100.1-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:8f53d917ece795e35c125ec24d342ddc2103a806c22b1f7b1517110d31f45362", size = 24063121, upload-time = "2026-09-10T01:42:39.437Z" }, - { url = "https://files.pythonhosted.org/packages/b5/8b/28f44f6fb841451fff57f66504283316628a80c13359b03819ea6c1967e9/litellm-1.100.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:63201b3ed92cbe6ff9ec26ee83436efbd67c5f6d38b2d1ece0203f04819fc4c3", size = 23766467, upload-time = "2026-09-10T01:42:42.668Z" }, - { url = "https://files.pythonhosted.org/packages/5b/9a/095fd6051c80923f883888aa90ffdcfb15faa958124d6b8bf134c122d5fa/litellm-1.100.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f150a169807a1b24c0ffc5bd1838f77659bf7449db8718e65d1be1bca517825f", size = 24163432, upload-time = "2026-09-10T01:42:46.28Z" }, - { url = "https://files.pythonhosted.org/packages/61/78/b1b701cd7342eea1e39aacb7c04e0971089bf9ede2b7b6788546bed25ced/litellm-1.100.1-cp310-abi3-win_amd64.whl", hash = "sha256:2f45760e61a624660444d110ae6475edc959e2906914c6c308c710c8be2ca742", size = 23974164, upload-time = "2026-09-10T01:42:49.789Z" }, + { url = "https://files.pythonhosted.org/packages/ae/cf/6b3a687ab0ab4caacc35faaf695f6862654e21e2a5dc703fa9e1df6d7069/litellm-1.101.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:7cc623a224c6f11a04367a682b095a1e08e5f6da75c990a650910db5f9777819", size = 23777389, upload-time = "2026-09-14T23:15:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/42/f6/d5bc3aa1944244fd186e65ca16e62158f0f474b40f99a89e568a5d441739/litellm-1.101.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:d4064024151ff2877e542b56c6bb6a39e0c3e6651abf4639af9346a678586e52", size = 23434831, upload-time = "2026-09-14T23:15:15.245Z" }, + { url = "https://files.pythonhosted.org/packages/ba/12/755e4d497b975911449102b8b74d3d4f770137c4467e3e8fe304adf22d66/litellm-1.101.0-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:cdf3351e394206e785bf339f1a39d9358ddf4e4f38129750bfdb5e4f5ceab8ef", size = 23568059, upload-time = "2026-09-14T23:15:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/0c/0e/561d7a314a08940688814be590544907fdfde9225dce35badbd79fc9ad4c/litellm-1.101.0-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:140ee0997324b8fd2405f7f8c26f42c0f364996dc4030187344a90cf27916667", size = 23944856, upload-time = "2026-09-14T23:15:26.417Z" }, + { url = "https://files.pythonhosted.org/packages/f1/df/d640eb6cf4a304be4ec289b72c1b1d9bd66c73e90167211d3a09ac37b2d8/litellm-1.101.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ad013161074676fc91b91d828f696300132cf3936d3f386c4701a41710d70aa3", size = 23643397, upload-time = "2026-09-14T23:15:28.664Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b6/9d69e95de9465cd23651c6623518d5e711d8776a0e58e6ea62066c9d9470/litellm-1.101.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85d88053148c5c6e016e495273eeaf8f3a29779b60d18977ec42e2a6bbd2d8eb", size = 24042718, upload-time = "2026-09-14T23:15:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b5/0a54001992c9a8d991f6ddd7532e3c4cb7dbdcc94220a37586513aab6f89/litellm-1.101.0-cp310-abi3-win_amd64.whl", hash = "sha256:e5601ae404b0f1e38ce1e65c75f880254485942d9c824453e3926f2323457fd5", size = 23844988, upload-time = "2026-09-14T23:15:33.632Z" }, ] [[package]] @@ -3439,7 +3439,7 @@ wheels = [ [[package]] name = "peft" -version = "0.20.0" +version = "0.21.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "accelerate" }, @@ -3454,9 +3454,9 @@ dependencies = [ { name = "tqdm" }, { name = "transformers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/08/02541a2c29be7c78698f73d438bc0e733b214f3f35ec5db79fca8da8fc61/peft-0.20.0.tar.gz", hash = "sha256:4769c8093a4ca145fd6fb3fd4dd50449675f5fe46434ad1e98b285a132d4b1d0", size = 880503, upload-time = "2026-07-28T13:46:01.85Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/91/56cc2b1b6824f5a4026750274240f26cd23465fb1a29cd14772d555433f2/peft-0.21.0.tar.gz", hash = "sha256:17f2b5a264439f4cd983c02e95954f2e948e3bcb85f75895ee307f94a55fd28f", size = 978334, upload-time = "2026-09-15T13:34:02.583Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/79/13bcabb8048126422d5c4b880575d40886c726f354db88cfeed4325525bb/peft-0.20.0-py3-none-any.whl", hash = "sha256:0fbba16ffebfad3de96e06f2da6860fd860292324b85b6141909fa1e26ea9233", size = 775777, upload-time = "2026-07-28T13:45:59.809Z" }, + { url = "https://files.pythonhosted.org/packages/13/f0/29c37002f5ef5cb5be54890114d5c8f726f0cb5072a8ba0880758c912bfd/peft-0.21.0-py3-none-any.whl", hash = "sha256:b64eb75fd9dece7401c70e675b8d9de024993b70483691b41c62876f0c7809b7", size = 832883, upload-time = "2026-09-15T13:33:59.775Z" }, ] [[package]] @@ -3570,96 +3570,79 @@ wheels = [ [[package]] name = "propcache" -version = "0.5.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, - { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, - { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, - { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, - { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, - { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, - { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, - { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, - { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, - { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, - { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, - { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, - { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, - { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, - { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, - { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, - { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, - { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, - { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, - { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, - { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, - { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, - { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, - { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, - { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, - { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, - { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, - { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, - { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, - { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, - { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, - { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, - { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, - { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, - { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, - { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, - { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, - { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, - { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, - { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, - { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, - { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, - { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, - { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, - { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, - { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, - { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, - { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, - { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, - { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, - { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, - { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, - { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, - { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, - { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, - { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, - { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, - { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, - { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, - { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, - { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, - { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, - { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, - { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, - { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, - { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, - { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, - { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, - { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, - { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, - { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, - { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, - { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, - { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, - { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/9a/9fbf4e4ec0c2d7f1c32519fff782ef467859b8faa9fbc5331a96f6395d43/propcache-0.5.4.tar.gz", hash = "sha256:ff6b113f50bc066a698db5d944d2c6dc7507168dd3341e255a8892fd0715a558", size = 61545, upload-time = "2026-09-16T00:17:14.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cd/348d58f142aebc4873345c6b31087629182ca6e0f2b3caeaa528cf882eba/propcache-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b28f41fa3b8c6900457f858ec5b03998f3a6d535fbc1bb2edec5961ea05ec429", size = 87285, upload-time = "2026-09-16T00:14:29.362Z" }, + { url = "https://files.pythonhosted.org/packages/df/f4/f3ffaee281b276da854ac1d7a6a506d26cbc62ea2e623756f1d0a4a1ba1a/propcache-0.5.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dcbf346a318a5e30063f547630b02bb787ce2f45b6368d5da143660b6a3835d8", size = 50984, upload-time = "2026-09-16T00:14:30.473Z" }, + { url = "https://files.pythonhosted.org/packages/25/88/1d7df7201750b37765ef2b23bc1c526c028dadde80afa0f57a118fc01182/propcache-0.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87a3caecf8095e48dc72f84bfa42e23a848cf410cc9cc13031fba4869b706a21", size = 52460, upload-time = "2026-09-16T00:14:31.692Z" }, + { url = "https://files.pythonhosted.org/packages/83/4f/48865bd02a16ee5236bc46166b2946f37b93e07b0eae355dac0be0b216ca/propcache-0.5.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60a64cbccaa11b7760ce705a14ada17ba459e7ca9f23ba587eb013821032d7ef", size = 251768, upload-time = "2026-09-16T00:14:32.908Z" }, + { url = "https://files.pythonhosted.org/packages/b0/19/3742a5eed62317b03b4002ee865dc9fd720308bdd0da1f29a5786c630311/propcache-0.5.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a74bfa37147cc08fb29df10bd9c16f40fa7f860cd3a6d2fff853323a94f6e17f", size = 257723, upload-time = "2026-09-16T00:14:34.267Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/ee6350fb0be9122bb6c67082a876d34b90d980d100c106af4b81023e04f4/propcache-0.5.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a4d7a54719b67338a305dca2ce6aafe366817df94ddfd4b5514374356f5ca546", size = 265597, upload-time = "2026-09-16T00:14:35.56Z" }, + { url = "https://files.pythonhosted.org/packages/85/9f/83a07b6ec0e043c050cfdd35fb0cf1b7897b91d554d6eea293740309afe7/propcache-0.5.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2814ecd8e818f487bee4b0f921bc4d1c176cc5fc71ac0f072d0fa67eda4ac14b", size = 250424, upload-time = "2026-09-16T00:14:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/33/2c/a763a8251f50fba042af0fb1f02bfec4b31381e40aff760db2be7b2e1f84/propcache-0.5.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6af4693716bfb03f1752ef1b30faa593db2c01d5272e9b8564a1549452a979ab", size = 216748, upload-time = "2026-09-16T00:14:38.369Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e2/4d11bea8fd6a777149c6c20645f873952eab5de3a2497aa11648ec9ab6ab/propcache-0.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4fbc1a15dc8cd1689508758d626b372b1f09d28d9577667feaf9e6bfcd8efcbc", size = 246533, upload-time = "2026-09-16T00:14:39.82Z" }, + { url = "https://files.pythonhosted.org/packages/9f/36/6683597de4907e70c717e3588c541202c66086a72ff3db58be49de66e72c/propcache-0.5.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cdee8205a44d0be91bbac4c41b95d86641b72dfc7aef1279400e4fda3f26a937", size = 238173, upload-time = "2026-09-16T00:14:41.259Z" }, + { url = "https://files.pythonhosted.org/packages/85/84/cb08d79f1762daafeb2b030c470cd0c725c97b8ad67412457c6f35c53e9d/propcache-0.5.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9a2a8a50a93dee0268a860a07fa3b4bd968f8ce4dbd794957da772f395368526", size = 251128, upload-time = "2026-09-16T00:14:42.652Z" }, + { url = "https://files.pythonhosted.org/packages/c2/0d/41b848036db6621370c1f2e5471a7da8149c730f8552a5257567721f4576/propcache-0.5.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7ffafcbfc7b549ab940047e505c831eabac5e67de53e1bc174adbc5285c55944", size = 214821, upload-time = "2026-09-16T00:14:44.112Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/adfae4bf9c63bccf12e2d9690a175c6579047a6eec3b5a6a5f51428c15e2/propcache-0.5.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d1f5a500bfcbb2c0ab85e98a0dcd70f5899d34efe365a0187700369a79603031", size = 254793, upload-time = "2026-09-16T00:14:45.429Z" }, + { url = "https://files.pythonhosted.org/packages/51/6f/eeca9647245d5f92e87d53e5f14335bb42fce1a7e6842c8045b364eded8b/propcache-0.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a235f73d6e020855dc29dff012d920c02ee0feab8d73a24185a7569f4be1161", size = 247134, upload-time = "2026-09-16T00:14:46.976Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a9/424e38838793d37160b4379c702f61c74c598fc6cd17204adbe3c554f7a8/propcache-0.5.4-cp312-cp312-win32.whl", hash = "sha256:b3083bfe87f95c756e610bd8025f26cbd1cd4aaa03a422f2d65efb7a97cd53d8", size = 43073, upload-time = "2026-09-16T00:14:48.338Z" }, + { url = "https://files.pythonhosted.org/packages/58/7b/6e8ef26f6d510a7916064fec68d55fcbfbdf7eb01e377480d66a122152d8/propcache-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:98914de2c4d7f0f9f4a8c6ea4bf05841f4175796941e3ef7d47eb718f22311fb", size = 46190, upload-time = "2026-09-16T00:14:49.99Z" }, + { url = "https://files.pythonhosted.org/packages/08/b9/72028c5b56ced97f456de6aefa79435ca64d7f77af78ea8cf3c76fc5195f/propcache-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:8876b39961e33d912afe3c1bee18ee564fdad0206f873cc15d522756b7f50737", size = 43075, upload-time = "2026-09-16T00:14:51.155Z" }, + { url = "https://files.pythonhosted.org/packages/78/4c/3b1365d58a667689e067e13d055fcd92bdf8d9a2fca3d9201b47ed5b3631/propcache-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:36c0d9db44b523ef93d03341b1c42d69ff01d673c053d1b1c6c3a363bcaa39ba", size = 85290, upload-time = "2026-09-16T00:14:52.342Z" }, + { url = "https://files.pythonhosted.org/packages/8f/61/5f9c29c3aa67c30238c4eadf95149b1d983a48f69b86b0cff927a7d6df13/propcache-0.5.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1d52a05dc417279f7e5c7618c5dfbbc29923aaf9bc0a5c1802ddcebf54c61a0", size = 50027, upload-time = "2026-09-16T00:14:53.67Z" }, + { url = "https://files.pythonhosted.org/packages/25/7d/c1ab1ef09e9d4d835be5d58c0a32a1e1de8397abaa4e502a9d4141328cad/propcache-0.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44149f46500a0a41b95b4d99c2e586a77319539730607b9892974a092788b111", size = 51425, upload-time = "2026-09-16T00:14:54.826Z" }, + { url = "https://files.pythonhosted.org/packages/73/36/0093091ebb270fcd1bc1f6e095f93b2e0ed7f1011c28837dc2dbe5f96b99/propcache-0.5.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbab5f5ff6897c81f355d079010cdae85b02e5a0b518b5251523b8ad8ae9ac3c", size = 233595, upload-time = "2026-09-16T00:14:56.09Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/0de9d4c8e05ce0be71b436919a216bd7fc5cc6e2691c0602295efb22b9ed/propcache-0.5.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3e98c55bde2bcf7db3c70d1aed7ae9aa8aebbf19a250c66645cde44cdb8b867", size = 240318, upload-time = "2026-09-16T00:14:57.674Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/2b35e91455209b85ee98f7859583e0814fab57d3af0f2381aaee34c37304/propcache-0.5.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db3ae52ccc150dbc84704e9d642743897f3e1c54742ff34cacb661e52e3818a9", size = 246649, upload-time = "2026-09-16T00:14:59.352Z" }, + { url = "https://files.pythonhosted.org/packages/ed/74/08e6c1faf26ee2732023a3828787ba535557122774f4a386b1f715cbd8e0/propcache-0.5.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f85915e00dcb1cd9f2f890ead064ed40a27df06f0db65be427b29482ae357572", size = 234316, upload-time = "2026-09-16T00:15:00.696Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/08385733c9321c9bb78039d3ff31045e4fca962d9665023c4eb70f998819/propcache-0.5.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c2ba30a89035b57b73e00475de948521602f543d79ce01db10b04b36c4c76fc8", size = 204666, upload-time = "2026-09-16T00:15:02.019Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f4/e87bc7629af9a14a752b218764a78742d73c2c563ac58315da6841f0cbe4/propcache-0.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ae58f361bd5dae942717c65d3413b478c70aea9c462599e7b9adad3731db3894", size = 225900, upload-time = "2026-09-16T00:15:03.394Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6d/11014938d3fe9bea2ea2dcf930f26ed565bfb2f5be3c756362ea48c92636/propcache-0.5.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:96f7c5c15656040ddcbc51e56dc59b58aa25999d743c126abd425b9766ab43e9", size = 219988, upload-time = "2026-09-16T00:15:04.811Z" }, + { url = "https://files.pythonhosted.org/packages/dc/72/fbf17c589f92c0b3bbf6709a425661f8ef2ed0d46b38985a7d7b5a0f6b91/propcache-0.5.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7cc528e760a8af06f2b13e9b9f362cd90c7c718ea61228a96dbd31ba16ed7f47", size = 233611, upload-time = "2026-09-16T00:15:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/55/7e/dbd637572a279692e5518d117274a9331bf5faac59f191d30e82521a3ec7/propcache-0.5.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:425f8cc86ab5018b4b8d4a23bc8e74d964bd3d757c3702e301aa79be76c53f6c", size = 204333, upload-time = "2026-09-16T00:15:07.961Z" }, + { url = "https://files.pythonhosted.org/packages/ba/5a/f99c92068f1e0f5c886899ce0e4a619db376ca98c5279d93f95bd86906af/propcache-0.5.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a5793c7698a53f56f4a1889a4737c7eeb1b7ad0842fa6b1abca22913ff79c8c1", size = 235177, upload-time = "2026-09-16T00:15:09.334Z" }, + { url = "https://files.pythonhosted.org/packages/ee/28/95456fabd2daf6be89049a13fbf03341756014d2959c83d12957d4c49694/propcache-0.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c02c0e570c5c7e077b0181a9f3cdb7d4c3617d1cda6b5c95bd5d34022923d82c", size = 228982, upload-time = "2026-09-16T00:15:10.729Z" }, + { url = "https://files.pythonhosted.org/packages/b1/bb/df90f62c9cf7c93ea235f6f9405143bba802914607317266dd81fc8d737e/propcache-0.5.4-cp313-cp313-win32.whl", hash = "sha256:3e413d7a4a9b4866b7a761d6060d434b64d23cd35122eda3b026a0bbe8196b25", size = 42611, upload-time = "2026-09-16T00:15:12.111Z" }, + { url = "https://files.pythonhosted.org/packages/01/bc/e0a7b84af04ec02d73a48aa71f091e1e4a2107e3074b7ce12195b66901f4/propcache-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:0c889f6fa84957bc7e8b4eab71fd16a0455068d5045e3aa40c733071d2b2fd77", size = 45342, upload-time = "2026-09-16T00:15:13.519Z" }, + { url = "https://files.pythonhosted.org/packages/9a/70/50b031cafe72a5c1878b903ee87303f71313345566bf3d6ec202e5ddc9ec/propcache-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:69fc35c0779522da366c563e5faf203ffc1f8ff0021d5b1337fa4efa5be73177", size = 42408, upload-time = "2026-09-16T00:15:14.788Z" }, + { url = "https://files.pythonhosted.org/packages/33/c9/07e227b930c8ae513b8ef1aae3793499be097bffcdf7aee4fb8b33db4cd1/propcache-0.5.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e6720ba44ad7e72174314d0e1fb0172494cff5c73a3a8a2159c3d2402ff15565", size = 85933, upload-time = "2026-09-16T00:15:16.073Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e1/6710bb44510c4e4a8e0f004bbaf3cecfd048141309c77bae56d4e5a6ebc1/propcache-0.5.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cfe0a92ae30151869e67a4b5f5e105e4e03ad30b3f38e5211b5bf77d0881993", size = 50179, upload-time = "2026-09-16T00:15:17.377Z" }, + { url = "https://files.pythonhosted.org/packages/e2/22/b533b493d7025456f44518b33e53e000021a20fe7c27b88cf3d341df7186/propcache-0.5.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d759d05634f1b038fb625a66662a8c85e5a8fec912da381b5149ddac107482b", size = 51942, upload-time = "2026-09-16T00:15:18.589Z" }, + { url = "https://files.pythonhosted.org/packages/f1/74/70ac8430e28f21e442c7bcb964eb46c4363f6881ade4aa0e978bfd8d503a/propcache-0.5.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:251c63dd46a0659bb875cb254dc4c1e79ee91a847c737cd62373295afc2235dc", size = 232647, upload-time = "2026-09-16T00:15:19.905Z" }, + { url = "https://files.pythonhosted.org/packages/72/95/f222f13b6fe623310be0eb61a673bf26df439ce27e563ca8e422d0818777/propcache-0.5.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a8d5ff04eb1f85698a78d20c62a14676e7b960dcafde09a388d60ad377d355d", size = 241541, upload-time = "2026-09-16T00:15:21.3Z" }, + { url = "https://files.pythonhosted.org/packages/a2/3e/763e370340db16115c5e63ad46e21ef0770a7f06928b3d3b62d8f8edfca4/propcache-0.5.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b9100a93b372418d8688f3f2a3e5b45c64d70ca4d6176e121aca1e3bfc1e32f", size = 245332, upload-time = "2026-09-16T00:15:22.802Z" }, + { url = "https://files.pythonhosted.org/packages/96/d3/e97cd6f5de2176bd90ed4076c7a9b5e09d0f0b9687d00a576507988bb62c/propcache-0.5.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc07876cfb079b6f6f36d21ce75784ad6c2c6b563eeac0ed26c2fa2669b85df9", size = 232757, upload-time = "2026-09-16T00:15:24.374Z" }, + { url = "https://files.pythonhosted.org/packages/f9/4c/6766e5f60bcda26d244333aa71d0a702c1c9b21b251d543c7af5953d1eee/propcache-0.5.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0951315a6b3142ee2167404d707743f0157c110091342b1aa0accac5cf0e4acf", size = 204389, upload-time = "2026-09-16T00:15:25.667Z" }, + { url = "https://files.pythonhosted.org/packages/b8/5e/ec4bb09a70b26ea99d76a8292c3383b960b296de2b347ac9986678f1761c/propcache-0.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bee7d3aed13d56f54e681df38c3a23031bc9e3863f687d9d598825c9146acd7d", size = 228217, upload-time = "2026-09-16T00:15:27.11Z" }, + { url = "https://files.pythonhosted.org/packages/e1/7d/b53922ba7d9e5bf797324e63aa05906ec240871899f779628df068743e2d/propcache-0.5.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4e985382be6d15da8d0c2710a6fa7b9070fc9ecdeefb7f580e88373984ec8be3", size = 216947, upload-time = "2026-09-16T00:15:28.532Z" }, + { url = "https://files.pythonhosted.org/packages/ff/39/b62eee45e5ea4de094a258cbb3b01c1e856ca51ddfd95b43135c5effd1eb/propcache-0.5.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e9ab13760aa8b6d0881ae7cb04fd891d8d490cd2554ea8e79bb278399169bcc", size = 233457, upload-time = "2026-09-16T00:15:29.977Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a9/feec61ed296d993db9dd097e0f6723e3f576a647722367547495e4c5b05c/propcache-0.5.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1b2f3bec4261a94019575481c726c29850f72e27907773c75b1de421e20e9f9d", size = 204131, upload-time = "2026-09-16T00:15:31.74Z" }, + { url = "https://files.pythonhosted.org/packages/92/4d/411ef380cddad28dc001f1c6d75ec72c76cd3817030f68ec1ccfba0ec6c1/propcache-0.5.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:720cf832eb2d0b0dfee129cb3335a26f6ce3cc45ee1187e8f0731758caa16792", size = 234820, upload-time = "2026-09-16T00:15:33.087Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/c988229753629ef1cfd5198337a83e624780ea2b3787efe9e747c05aad2d/propcache-0.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fb0a5be8d9aa213150e8d8148a42aca4984b285bcad1e69587dc4298edd929b", size = 228350, upload-time = "2026-09-16T00:15:34.533Z" }, + { url = "https://files.pythonhosted.org/packages/12/49/5ef1c5cf98591da3c5b952b39e6a298084cc1ce353bc70f85e82397a5036/propcache-0.5.4-cp314-cp314-win32.whl", hash = "sha256:30cc1cebaf9aef49db06357a50398323ae04d70460c0491837d026ab7d6452ea", size = 43578, upload-time = "2026-09-16T00:15:35.957Z" }, + { url = "https://files.pythonhosted.org/packages/1e/9e/a0ac821a2229186af5e2e3c3635a78abb23cfddca57f38513ab5d70420f3/propcache-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a095db8e15a6020db149ecbed6461939fe74f6acaa3ae8b702a1fe8c38cd983", size = 46304, upload-time = "2026-09-16T00:15:37.655Z" }, + { url = "https://files.pythonhosted.org/packages/a1/19/c8d0d36a9d16cba5dcee67d389c9333b988c8986a653a61c00a451817a46/propcache-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:45488d1a5f9ab5bd90aaa1ca20f50fe1922b8ffad71a2009d2adf41355897aac", size = 43440, upload-time = "2026-09-16T00:15:39.091Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e9/42f1da77cacfc184e6ec929557ef653b7961bbf6f1da460b9221273948b3/propcache-0.5.4-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:53eaa697c4d0422ff4cb714d00231b43352064d97b944033b30c1d57cc506ec0", size = 90672, upload-time = "2026-09-16T00:15:40.306Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2f/4b79940908c6ab8c795097c102999d7bc1f7e0b8604dfd1c232f9d99d67a/propcache-0.5.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:886b59c4d28ca97dd23b025fdfc50a0356be934efbbbca89ad26230067f86fe5", size = 52586, upload-time = "2026-09-16T00:15:41.575Z" }, + { url = "https://files.pythonhosted.org/packages/eb/07/02196ae6320c110235bb343f90dbd34be41f8b8964a3ee30db84ec12579e/propcache-0.5.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fa15757fea1dfcd5b7745cad9f4638929605531bd4018ab2adff7955f1a403d", size = 54335, upload-time = "2026-09-16T00:15:43.027Z" }, + { url = "https://files.pythonhosted.org/packages/6f/44/f48b9a131985659924df5fa5093f68fe72c7ee375329802989ba3126efc6/propcache-0.5.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f0093ac3e9daada202c2082439d414a625c57184727a46e112a3fb2a81cb788", size = 297567, upload-time = "2026-09-16T00:15:44.373Z" }, + { url = "https://files.pythonhosted.org/packages/04/a1/418d956d2735139f77fc35262179f1f52c23aa666de5a8ab3819c1ae7854/propcache-0.5.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3cd3a7edb6b95b9b33998135ebfa18d709da82290fb8f27c858970b5a12c8b56", size = 297477, upload-time = "2026-09-16T00:15:46.048Z" }, + { url = "https://files.pythonhosted.org/packages/69/fd/ff811fdb6d3d3e67fd9bbfb75881675d34a42d0ef29a45d33e3e233dde07/propcache-0.5.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c174bfd1c48a1b51a3078e95586dde718374bac79719ab3541ec9e74aec40574", size = 302669, upload-time = "2026-09-16T00:15:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/fc/57/527910c455b5ec62f6871bef45d4f79fea16cb8c966ba0d4a07f0339ddc4/propcache-0.5.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a219f0ac59817a9114dd2aa57c13180f993e819ba658c7ddab4b66ed1ee0d370", size = 287908, upload-time = "2026-09-16T00:15:48.99Z" }, + { url = "https://files.pythonhosted.org/packages/1d/86/f69ab82707534a0cb2057bdca04f9200a71214c7551800f9d34d6ac39e4f/propcache-0.5.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:17a7400cec0256f0a71ae71f9da398f9894c956ff6668a1c9d317b3367316320", size = 249804, upload-time = "2026-09-16T00:15:50.486Z" }, + { url = "https://files.pythonhosted.org/packages/27/19/60677af50d93be4256213de7cd487f056944c048b9c0b6f2e45b3a30f666/propcache-0.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:978f28401afbc76cdc3df9e1717b4229a06b626a1dcc75db4e1f2beb3884c3e9", size = 282344, upload-time = "2026-09-16T00:15:52.029Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f7/a0057808a91fb3b6a5f3602b528f0cdcb3d53e0ff8315d73fabdfdf8fec4/propcache-0.5.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4a1f4f5ffa55dce6307631f3cb2948e117e665966ea512e0d502b16c24f567e7", size = 270167, upload-time = "2026-09-16T00:15:53.466Z" }, + { url = "https://files.pythonhosted.org/packages/83/c8/f4a865490df0dc0c8531d4e59ac411cb6dc24bb255d2396a6f1c60a368f4/propcache-0.5.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:213bb68d9ced5cf2bf717b1071bf2b09b4b04c426256f9fe6d054c60318424c4", size = 286551, upload-time = "2026-09-16T00:15:54.995Z" }, + { url = "https://files.pythonhosted.org/packages/b0/67/b4faebde9da4e8173d0e5a30e8cd31335914af7ef350b988f27fec588cfd/propcache-0.5.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:286867fb156488c251a3721766e380ac4495e4fd6b51aaa1403d89ce7f4359d9", size = 249595, upload-time = "2026-09-16T00:15:56.505Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/52e1dd5636e9f5a27f6b5a4b4e2f33c322fd72afe956c397d82523ec4a80/propcache-0.5.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:445ee3bfb46e85838387fb3c536a73cc0b994dc192b004e40e170adc54aa2a7e", size = 286700, upload-time = "2026-09-16T00:15:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0e/30b2b324b93ff31a0bab539c102aae59e84e444031b2742150a7646aa1bb/propcache-0.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:48cb48c5346a97de792254af77715aa2529c2a1ebc5f586aa0aae44a02f1fe57", size = 280500, upload-time = "2026-09-16T00:15:59.487Z" }, + { url = "https://files.pythonhosted.org/packages/64/36/721bb59f682ff060d0c8df64274fca8cd0521b1a54506c2eedaef795b7f5/propcache-0.5.4-cp314-cp314t-win32.whl", hash = "sha256:03b229037d25b801e7af53fd52b9fc49d9439b036fca1e087e02780631adfa97", size = 46121, upload-time = "2026-09-16T00:16:01.349Z" }, + { url = "https://files.pythonhosted.org/packages/c1/86/0b1b80fa1ac3a0aac44e2922a6964fbe9cd52af5eab8fa933bf9e90b030c/propcache-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1fc236528c457cd739c88abe823da851b7ab645d72792f88658114cc340c12", size = 49154, upload-time = "2026-09-16T00:16:02.901Z" }, + { url = "https://files.pythonhosted.org/packages/69/4f/9fe6f05a47cb550c823155052116f710064b6be5c6e8ec4e9faae7e18115/propcache-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:135036c5cfc93864affb0f9af9a27e5d7a71cb7bd745e7b6dbfc2d56cc30e827", size = 46005, upload-time = "2026-09-16T00:16:04.266Z" }, + { url = "https://files.pythonhosted.org/packages/f5/cd/785c64ed382f3f04201870267b02783f63b4678c2acfddc177a3ebcc2727/propcache-0.5.4-py3-none-any.whl", hash = "sha256:62c60aec739ed00124573cce1178138fd690c7676352d67a37328c1cf51d7468", size = 16338, upload-time = "2026-09-16T00:17:13.106Z" }, ] [[package]] @@ -4190,11 +4173,11 @@ wheels = [ [[package]] name = "pyproject-hooks" -version = "1.2.0" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/94/21931ee768a9544afa6330a2e790e6ef8ce70568e88d79b15560c3054e91/pyproject_hooks-1.3.0.tar.gz", hash = "sha256:26dfd4229ff1820b7964be33bd3eb64367eb7c841215defe11341450c219ce20", size = 20883, upload-time = "2026-09-15T17:43:43.445Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, + { url = "https://files.pythonhosted.org/packages/58/86/3276e42c9349f2349a8a0fa6abf145c867ed7719a56c907e6ef21edf1462/pyproject_hooks-1.3.0-py3-none-any.whl", hash = "sha256:733909c6ac133d222c0b8cfc11364f19d363793d8fbcd8bb7240c5220af8d35d", size = 10723, upload-time = "2026-09-15T17:43:42.11Z" }, ] [[package]] @@ -4896,15 +4879,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.69.1" +version = "2.69.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/16/85874f5e51f8d0767ee8c4b4c460c5ea2bc8a1b613d641d9d9577ba39d3a/sentry_sdk-2.69.1.tar.gz", hash = "sha256:f9284b417540b0784b994fa021eb6f1e30ae1cce593d83541274d03c93966eff", size = 1043599, upload-time = "2026-09-08T14:18:19.505Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/08/1211e4652384195ef754f8ed570566088dc848f8eda7d005abf13f05f17f/sentry_sdk-2.69.2.tar.gz", hash = "sha256:b4d8915a526e626b0b14a2925907554b722d3b8e3e3781570fc35791712af323", size = 1043564, upload-time = "2026-09-15T15:25:09.018Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/eb/17040f8a60c300fe4cc349088d3e351b88812f478b57a0af05a49af7cb7c/sentry_sdk-2.69.1-py3-none-any.whl", hash = "sha256:2d2556d9a14db548982b914cbed2a2dc07b6e55d941a620752f89a2afccfd78d", size = 528587, upload-time = "2026-09-08T14:18:18.151Z" }, + { url = "https://files.pythonhosted.org/packages/ee/78/5d8b79fd60330234317aa827a1f5e445980a6f4e0ea6ed4e1187acb70a0f/sentry_sdk-2.69.2-py3-none-any.whl", hash = "sha256:25bc0a6c55bba0b83398212a7247f4f9f3190f189a1909b68e4807ef4b785895", size = 528765, upload-time = "2026-09-15T15:25:07.362Z" }, ] [package.optional-dependencies] @@ -4959,37 +4942,43 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.52" +version = "2.0.54" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/21/77b4c147963073040dc3c3a5cb7a8c3001a1893c0209432cb77f9df836aa/sqlalchemy-2.0.52.tar.gz", hash = "sha256:5e2d46356ac2ccb7d268ab6c2319ac6a2b42f1b8d5fd8bd3d46855cd82abee97", size = 9945637, upload-time = "2026-08-11T19:07:09.829Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/d5/1b77a026d161f98a08f11af1a5f6c47b98ee7c7e2648af525a1004826c78/sqlalchemy-2.0.52-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be8c49131665dfe2cc74c498aa1240ffb548d0fd901325dd11c2c7a18956f727", size = 2170940, upload-time = "2026-08-11T20:58:11.25Z" }, - { url = "https://files.pythonhosted.org/packages/54/bd/f444444adb37b5d53753fb1730ee7a421628e2e3b756c4da461af7e6394a/sqlalchemy-2.0.52-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b2d9e507a458832adcfbd8af6e2036ddf069b7710b799448542ebccae2dceee", size = 3383415, upload-time = "2026-08-11T21:02:38.534Z" }, - { url = "https://files.pythonhosted.org/packages/be/57/2eadf93a552568c57e8680b7e58bb5e9770d80942a1bdbaf4f2f63f0d7c8/sqlalchemy-2.0.52-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8738008376d22f30f411ea3efecf39b51110b6996d80bb73786f30bcfdd5fd3b", size = 3398577, upload-time = "2026-08-11T21:16:59.092Z" }, - { url = "https://files.pythonhosted.org/packages/15/c3/2887cf9dd111d1fbf05d22165b404c221ef43e029f7a2695e7302f27a7cc/sqlalchemy-2.0.52-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37a4d548327b6cab9c7d8cdb4e0e82feabee0110c4d150059068e2d1cfbd99ee", size = 3328225, upload-time = "2026-08-11T21:02:40.183Z" }, - { url = "https://files.pythonhosted.org/packages/02/0f/466bdf9e1feeeef5587f868c187d8687e21ff8c85b1775e9041130181132/sqlalchemy-2.0.52-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e49f51a5d59857a7a0dcaf9469febf7197d9394bd88f00d69c2c4e848112cdbf", size = 3357374, upload-time = "2026-08-11T21:17:01.076Z" }, - { url = "https://files.pythonhosted.org/packages/22/20/5c2b4583904af4173076dda1c9e53c9e2ffc7a702d2efde0216bbacbf7cb/sqlalchemy-2.0.52-cp312-cp312-win32.whl", hash = "sha256:afda3ec521d0517d0de783fc70030775841900896d832de5bbd066549290470e", size = 2129366, upload-time = "2026-08-11T21:14:50.991Z" }, - { url = "https://files.pythonhosted.org/packages/ed/06/543dab8ef62d4e9fb96fb31a30c2b8b14a8763bccf48d428294d6b3041c0/sqlalchemy-2.0.52-cp312-cp312-win_amd64.whl", hash = "sha256:2d5e53e36e37129fe0be8b9d08b6e4052c10a963ee6cda56c8c10dcc194b99ca", size = 2157344, upload-time = "2026-08-11T21:14:52.453Z" }, - { url = "https://files.pythonhosted.org/packages/7f/18/e30c6fe1eca1bf34a39fbdd6066121cc9974c850faf6f349eac563697a26/sqlalchemy-2.0.52-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2eb3c6a64b1bfe6704777cfd504e7b8ad093a5f3e03ce67663a5e6742f294e43", size = 2167724, upload-time = "2026-08-11T20:58:12.679Z" }, - { url = "https://files.pythonhosted.org/packages/d0/56/2e17d161a4f7ecc1c2ffb93e607b4e1898bb551b451b283235acb8f6ce47/sqlalchemy-2.0.52-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:923bb183c1dc64fdf7b717965e3d59938ec4f8b8710b419a21ce403e5da9a9e1", size = 3321189, upload-time = "2026-08-11T21:02:41.932Z" }, - { url = "https://files.pythonhosted.org/packages/cf/b8/8490916e893f3f8d74dc9cc54c078619364999dee37047a188e73abbc852/sqlalchemy-2.0.52-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:651d6d8782e80679e6151707c7b490834d46ada526328895abf567f25e63d29c", size = 3338185, upload-time = "2026-08-11T21:17:02.597Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f7/752cc8ee453da222829b3f5c4613614bf750d97429363b70414fa10478e4/sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b08cddb8989775e3c88799d86704bdfc3ee6e9846118201aa5997f16f27e3a15", size = 3271698, upload-time = "2026-08-11T21:02:43.963Z" }, - { url = "https://files.pythonhosted.org/packages/51/e6/074ade0c07b9e4c8e8bca46820320ed94df9702afdb6f2af06623068d2e6/sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab66fa9618269390d4dfa222f2f2f88f7bc4bf5da13905131b818217db7e8057", size = 3308936, upload-time = "2026-08-11T21:17:04.172Z" }, - { url = "https://files.pythonhosted.org/packages/66/07/557c0d04716705599227945ac14e0a17ad0338e899f37d8c2ddff4dcc663/sqlalchemy-2.0.52-cp313-cp313-win32.whl", hash = "sha256:c63bda077685c85ca513286547a531ba57e7a68cf0a7ed3bafcc2bbd18896f4d", size = 2127308, upload-time = "2026-08-11T21:14:53.879Z" }, - { url = "https://files.pythonhosted.org/packages/96/4e/226eda27654318ce525d043025221f689abef883da2c7126f9065121618c/sqlalchemy-2.0.52-cp313-cp313-win_amd64.whl", hash = "sha256:9876b09b9f1ce7398b0ffece585c0a911244c53191187341f6bcae640e133751", size = 2153876, upload-time = "2026-08-11T21:14:55.527Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f5/71cb30af58c9b80a4e1fac0b73bb48f86d497a774a6a2eb6d2f1e657bb73/sqlalchemy-2.0.52-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:410d52be41d17f1a236d19520fbe776257dc16516ed06bd16d433311842aefd9", size = 2169537, upload-time = "2026-08-11T20:58:13.855Z" }, - { url = "https://files.pythonhosted.org/packages/4c/93/d07ebd645d1b07b6b5ed63450a70f063a346a7e0f2c8810daf2e532400cb/sqlalchemy-2.0.52-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfe9ce533dbe4d0a2ae1486546619bd30b76bcd670539a44d910361376175f5e", size = 3319606, upload-time = "2026-08-11T21:02:45.829Z" }, - { url = "https://files.pythonhosted.org/packages/ae/5c/290c84c7c2566ecd3b65baaae0fddec9bc33b033b398a06123bb86fbfc6e/sqlalchemy-2.0.52-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:812bae5138bfc0aa46fb0686da0fc7f581f68e2bbb05bc24c3713bebaedd1437", size = 3323642, upload-time = "2026-08-11T21:17:05.675Z" }, - { url = "https://files.pythonhosted.org/packages/13/f5/2cc160590ca49173359557880b92a0572293ccb899e8f6cedf150c5a3ddf/sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:50bff43b632a56fbf5ed9afdd76307e1512b62051bcd5afb341ae67205bbb6c8", size = 3268125, upload-time = "2026-08-11T21:02:47.649Z" }, - { url = "https://files.pythonhosted.org/packages/35/f3/ea8933fc9f7d1353e9c2ff9965eae687c4cef181120574591ed2fa0633e1/sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:49565daf5af554f538e23aef1fc81a95a4e49658f152285e45c02f5fc44f04cd", size = 3289516, upload-time = "2026-08-11T21:17:07.267Z" }, - { url = "https://files.pythonhosted.org/packages/45/67/05cf86541c1e1716fca1e4a996954a439cd74501707cda607fb7cb02ef50/sqlalchemy-2.0.52-cp314-cp314-win32.whl", hash = "sha256:ab9da41e61b9979b910499d633b241df20c51ee5037e5405b11c2faac3cbe1a2", size = 2130249, upload-time = "2026-08-11T21:14:57.273Z" }, - { url = "https://files.pythonhosted.org/packages/96/d7/8ac6ffa1e36169e762ef65bd835046abb2251b1bc17f8f6708e14ed8d31f/sqlalchemy-2.0.52-cp314-cp314-win_amd64.whl", hash = "sha256:a593db51b3bae75db17a5738ad5f992244b3a03863f83c28117ee482c6a3f76d", size = 2156718, upload-time = "2026-08-11T21:14:58.667Z" }, - { url = "https://files.pythonhosted.org/packages/dc/4b/e01a737eef378e734cc6394a82248a6ce13b167dfa36c731075ce9fc9c64/sqlalchemy-2.0.52-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1e61d08bdf4ee2f41024569e3400de7d6734ba498144766b11260936ccfa582", size = 2190344, upload-time = "2026-08-11T19:53:21.393Z" }, - { url = "https://files.pythonhosted.org/packages/b3/3f/3582293d1e185e71d19d7c731c3e2ee20ba21981c4a1115c0806c1f62120/sqlalchemy-2.0.52-py3-none-any.whl", hash = "sha256:3b81b8363a919ce53453591cdb93702e6bd54ade6c4fa2f468fc053baee5ed89", size = 1950700, upload-time = "2026-08-11T20:47:21.603Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/271aa905cf2964f841371a97f3e63ab692bf51b4423d0491e67bc7f64037/sqlalchemy-2.0.54.tar.gz", hash = "sha256:baa8521e8ee9f24e75dfc7aaabc08020e551ef0d48d7c3e3536f5cddf277586b", size = 9969559, upload-time = "2026-09-15T21:06:57.337Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/30/75504fd1d70458000e85a3e772333dd0fbf80254b0dc4d41c99922e4d112/sqlalchemy-2.0.54-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffba7eb2d67c7505e82a0902aa854d8824b74c28a183820d6a8bd3cfd0f812c2", size = 2187596, upload-time = "2026-09-15T22:32:37.031Z" }, + { url = "https://files.pythonhosted.org/packages/a2/3d/5dfbb9528a391186a99986daecc5cbe003f34408dee91db8f5cdcf917040/sqlalchemy-2.0.54-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63cae7210fea9899e0bf35c1f1ae55d3ddd9c6d47cae8b6b43d945afa79dd65b", size = 3448745, upload-time = "2026-09-15T22:40:18.168Z" }, + { url = "https://files.pythonhosted.org/packages/d9/93/34fdc4a4faced77037a6b3ba1db1acd92e9bd12c21e229b327edd1d3e881/sqlalchemy-2.0.54-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:68d994e9b0d0423a02a20039631fa6fcbb7fa829a992f7605025774940305d19", size = 3466644, upload-time = "2026-09-15T22:35:37.7Z" }, + { url = "https://files.pythonhosted.org/packages/66/68/4beab40ae60ac3d679dbbb46bc0d2bb277013264a0bde37ecaf4b6780e98/sqlalchemy-2.0.54-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3de32cc6721eb42c3aad35bcfb244bb7a18f66c00f3582aae6281d6287a339b5", size = 3401519, upload-time = "2026-09-15T22:40:19.907Z" }, + { url = "https://files.pythonhosted.org/packages/e5/df/a24757e3249b1c7c1c5f0317666d16a8ab901ba239819ecef29eb4ed172e/sqlalchemy-2.0.54-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d31a2bc06a854ee52dd86b455be4df7c750b28817e2d1b884e31fff126c4fd7b", size = 3432590, upload-time = "2026-09-15T22:35:39.434Z" }, + { url = "https://files.pythonhosted.org/packages/d9/0c/69559e3d90d200fbfe9c44aff39a9ff4e506b1a3e9557a203d69df721244/sqlalchemy-2.0.54-cp312-cp312-win32.whl", hash = "sha256:32de6deded25e8b9b11d07428d496ff24dfbc882b8e990c177266948cb5f3d9e", size = 2140825, upload-time = "2026-09-15T21:25:22.444Z" }, + { url = "https://files.pythonhosted.org/packages/d9/10/4a0f7113664c2877906db143709c52ddbd28a88e4d51f0b520bc30048ba7/sqlalchemy-2.0.54-cp312-cp312-win_amd64.whl", hash = "sha256:d65f8ca742ef1e1e14bc417ef59dc2ddf207a7b66b30cfdc6152447314e030cf", size = 2170005, upload-time = "2026-09-15T21:25:23.885Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/021ccd7a838425f52e67c02edc541e8d53be8a27a103e684e14b18aac17b/sqlalchemy-2.0.54-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b374e3bc91e246a942592a98ba6a23be76fff21358b00546ac8c0ebc0fd0e00b", size = 2184259, upload-time = "2026-09-15T22:28:59.153Z" }, + { url = "https://files.pythonhosted.org/packages/74/8f/f95de908a4af7ac3a6925cdcb837a93274f608925a0fbc786df38811bccf/sqlalchemy-2.0.54-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31d5458672a6f72db2c087f4a5098b3c8503ea0254186ff29205d63afa9401a4", size = 3398792, upload-time = "2026-09-15T22:29:35.218Z" }, + { url = "https://files.pythonhosted.org/packages/84/26/bd327a1a6be223e438c98ac8faf299307d83797225a81f3b209607a9fd98/sqlalchemy-2.0.54-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cad78d04254967bdbcccbed5e631d88fe4868530946ab0929aa45e9032849518", size = 3417562, upload-time = "2026-09-15T22:40:32.637Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b1/f13a8fe8e8e167d8e65484b91b827897b8a5ca1efd7ba6e87a5f63412fed/sqlalchemy-2.0.54-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:48611087a75d26d798003645c688c7d3cfc26b89dbe4a2c568d6b378d330deae", size = 3349138, upload-time = "2026-09-15T22:29:36.729Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/5fd58f03281a74c57f02f509433e1be58a79d9f5b0e14020e665e0dcb614/sqlalchemy-2.0.54-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d6adf80277372a89910a0f3ccfe960b846d279dc55b366dd5c5ec07f41c84758", size = 3382943, upload-time = "2026-09-15T22:40:34.587Z" }, + { url = "https://files.pythonhosted.org/packages/4b/73/e29fa88dfc809857a55e6523911d345d00cab4adf87032c21ade8874223c/sqlalchemy-2.0.54-cp313-cp313-win32.whl", hash = "sha256:264460333ed0b177cbb1956355d0ee4e0cab83fb415c934ce12a25db2e7be39c", size = 2138931, upload-time = "2026-09-15T22:42:51.801Z" }, + { url = "https://files.pythonhosted.org/packages/89/9d/885e7491836f3dad368c74f0d5b93551d37b0ec66c9e8753240ee988308f/sqlalchemy-2.0.54-cp313-cp313-win_amd64.whl", hash = "sha256:cf89e92bf0d4204a6afcc17af27b9271ed9c7e34e17d6f80c085d431ea4a1747", size = 2166496, upload-time = "2026-09-15T22:42:53.603Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c0/4a6503c9d22d6d00a5631082ab1484222ecf7d573db791e0f53161bf7745/sqlalchemy-2.0.54-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:abd6b21bc58e91c1932eb5d6d7f1bd44a551dfec7b6a7f517c3638ccd67233a0", size = 2185899, upload-time = "2026-09-15T22:29:00.581Z" }, + { url = "https://files.pythonhosted.org/packages/12/28/f4424f618bd1f373761a32a821d53ce2c257350e9894bae9b968cb03d8fd/sqlalchemy-2.0.54-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5417322b3c025dd82918725d3bf09ec105fac95efc195722b8b06e1d9c381139", size = 3394763, upload-time = "2026-09-15T22:29:38.131Z" }, + { url = "https://files.pythonhosted.org/packages/59/d2/7f0c77f8e042cb5f28275fea29c3080b4ac6fd4b3fdd7f59ff1ef3e28c11/sqlalchemy-2.0.54-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f84099e4b04a5c2d44500a2a8302eee5af4bc6fee63e8c6e9cf6786e747280e", size = 3402800, upload-time = "2026-09-15T22:40:36.509Z" }, + { url = "https://files.pythonhosted.org/packages/af/32/3eaa930bcf71d17a72587081d2706a5fb97ab3f11a7e0fb838f581f7cff1/sqlalchemy-2.0.54-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a0956dc754d3884da7fe60097110ec7a8a105d26afa2f0844468f4b1598c6912", size = 3341469, upload-time = "2026-09-15T22:29:39.682Z" }, + { url = "https://files.pythonhosted.org/packages/eb/cc/cddb6cbd4408e5c55b3bf722be26b9d3b54d42509f901b7ecc15debd3d1f/sqlalchemy-2.0.54-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:87ba8834318b0d8dc94fc6f405d071b5c08be32a6c3fd68107fd6952ee949615", size = 3373232, upload-time = "2026-09-15T22:40:39.181Z" }, + { url = "https://files.pythonhosted.org/packages/34/2f/9c2aa5efc642b7f3b985d13565cd1a5e78856e079ef3022796fea5180498/sqlalchemy-2.0.54-cp314-cp314-win32.whl", hash = "sha256:842540e4382472f23c79589995752648d14696a8200d0807ed8c5c59c92ade44", size = 2142018, upload-time = "2026-09-15T22:42:55.118Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0b/3594f1f51769feb3022d686135dc5d8682a12345ed15ae61d0c0ca42cbee/sqlalchemy-2.0.54-cp314-cp314-win_amd64.whl", hash = "sha256:f4e8f955d13af83fb4e35c3472e5377ee22d3445eada1e5e48199588edb69835", size = 2169220, upload-time = "2026-09-15T22:42:56.727Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a5/c211a9a7af83222509519407e16a4db760c6df3d03be69ebc5414d465321/sqlalchemy-2.0.54-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ca05f4e7852cf48083b0cf157e4f9504b7068780422a50fa82f45353b8c5e14a", size = 2208458, upload-time = "2026-09-15T22:30:05.718Z" }, + { url = "https://files.pythonhosted.org/packages/cb/2e/490ad7b3731116cb48ba170f7722eaa99a89707193e54389ca84b7ad55af/sqlalchemy-2.0.54-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18a8b6417cbb7b735cf91c2b59453c2a554cefa0a8d7bd15aa35740739410d77", size = 3660585, upload-time = "2026-09-15T22:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/eb/25/15dfe6814847eeda773bd58ab6cf42a94b0176e5cf25a578fc1165777160/sqlalchemy-2.0.54-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e55a0b96a1577a1e108c91ccdeeb9cd92768f28ce206597311c3bf6d6423abd", size = 3624442, upload-time = "2026-09-15T22:36:38.377Z" }, + { url = "https://files.pythonhosted.org/packages/aa/19/724d0a6a2fb2a86ff2d6008e581c258f722d9b7d8e52adc7b79085febdd4/sqlalchemy-2.0.54-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:69cab115c40fd02c5a22c68e4ee630fa6ef9a1650f1de944419aab1f7096fc4f", size = 3562972, upload-time = "2026-09-15T22:36:08.581Z" }, + { url = "https://files.pythonhosted.org/packages/49/bb/9df1bd81c2f2d000cf5e7a1b1a9b331468a3aab939ad983355e701fa42b2/sqlalchemy-2.0.54-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e08397c6c42f53b2488acde9108b8bfefd52d7afd1bf2f03d2ffcab7a204aceb", size = 3576479, upload-time = "2026-09-15T22:36:40.272Z" }, + { url = "https://files.pythonhosted.org/packages/df/c0/b5775465d3b89061d7c46057c31c56ff8fb6c509550b2b0c6570ffc248b3/sqlalchemy-2.0.54-cp314-cp314t-win32.whl", hash = "sha256:b9086b8ad48280ef6a7ba68262d5e44f7db1c4cb1973e8cdae8a9f467ae66f51", size = 2174794, upload-time = "2026-09-15T22:31:45.925Z" }, + { url = "https://files.pythonhosted.org/packages/77/f8/296c2e46b4ccd3f29b00b954ef2f195dde32f98e352ed21de1d292cedc0d/sqlalchemy-2.0.54-cp314-cp314t-win_amd64.whl", hash = "sha256:b67c1744e453af833667fc1b84de07adb4a64f3536ef52a8ec5ac2b941d43970", size = 2211942, upload-time = "2026-09-15T22:31:47.368Z" }, + { url = "https://files.pythonhosted.org/packages/24/a1/bd5e3e99bc9c8863b51ac5b9b03008a7f2da8c6b59695992f5c654e1265b/sqlalchemy-2.0.54-py3-none-any.whl", hash = "sha256:7e33a631ab1474f8fe6b910bd1a07b7b8009c4c78cdd3fb18001b03e3bc2e1d2", size = 1958015, upload-time = "2026-09-15T22:24:22.95Z" }, ] [package.optional-dependencies] @@ -5090,11 +5079,11 @@ wheels = [ [[package]] name = "threadpoolctl" -version = "3.6.0" +version = "3.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/dc/6c58154c1c65f758ea979e7139cb76993a9cfc662d14e9be3c4a667cfb77/threadpoolctl-3.7.0.tar.gz", hash = "sha256:61348cfb77d53b9242e0017029244b559b810c142ced65b4e21eeca1843959a7", size = 31961, upload-time = "2026-09-15T15:46:20.263Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, + { url = "https://files.pythonhosted.org/packages/43/3f/f88a53f60a472b46f4023f56d204dd7de33d34c5d2acbfa0d70a674e639e/threadpoolctl-3.7.0-py3-none-any.whl", hash = "sha256:cd8b60b5641b45c67bbf73c64c843235fc2d8a480c87389f52f5dbee893b86be", size = 26362, upload-time = "2026-09-15T15:46:19.168Z" }, ] [[package]] @@ -5445,11 +5434,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.7.0" +version = "2.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, ] [[package]] @@ -5764,84 +5753,84 @@ wheels = [ [[package]] name = "yarl" -version = "1.24.5" +version = "1.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/33/ebe9e3d1f86c7a0b51094c0a146392045ca1631d2664889539dec8088a33/yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f", size = 228679, upload-time = "2026-07-20T02:07:45.435Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/84/71d051c850b5af41d168c679d9eb67eb7c55283ac4ee131673edf134bc4e/yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d", size = 136035, upload-time = "2026-07-20T02:05:25.489Z" }, - { url = "https://files.pythonhosted.org/packages/03/4d/8ad27f9a1b7e69313cca5d695b925b48efe51208d3490e0844bae97cabc0/yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec", size = 97642, upload-time = "2026-07-20T02:05:27.429Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b4/05b4131c407006cd1e410e9c6539f16a0945724677e5364447313c15ea3e/yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c", size = 97323, upload-time = "2026-07-20T02:05:29.441Z" }, - { url = "https://files.pythonhosted.org/packages/20/16/e618c875c73e0e39611f20a581b3d5e8d59b8857bf001bee3263044c6deb/yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54", size = 107741, upload-time = "2026-07-20T02:05:31.367Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9a/c4defeaf3ed33fcb346aacf9c6e971a8d4e2bde04a0310e79abb208e7965/yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12", size = 103570, upload-time = "2026-07-20T02:05:33.303Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e7/0e0e0de5865ebd5914537ef486f36c727a59865c3ac0cf5ff1b32aececbf/yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d", size = 115815, upload-time = "2026-07-20T02:05:35.292Z" }, - { url = "https://files.pythonhosted.org/packages/2b/27/ca56b700cb170aba25a3893b75355b213935657dc5714d2383354a270e62/yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1", size = 116025, upload-time = "2026-07-20T02:05:37.503Z" }, - { url = "https://files.pythonhosted.org/packages/d6/d0/d56c859b8222116f5d68459199f48359e0bf121b6f65a69bf329b3602ba0/yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9", size = 109835, upload-time = "2026-07-20T02:05:39.506Z" }, - { url = "https://files.pythonhosted.org/packages/70/a2/3a35557e4d1a79425040eba202ccaf08bdc8717680fc77e2498a1ad2e0a5/yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027", size = 108884, upload-time = "2026-07-20T02:05:41.584Z" }, - { url = "https://files.pythonhosted.org/packages/e4/35/ef4c26356b7913c68983bac2d72a4212b3347af551cb8d250b99b5ed7b7f/yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b", size = 107308, upload-time = "2026-07-20T02:05:43.697Z" }, - { url = "https://files.pythonhosted.org/packages/d5/91/ff0dc66c2ccf3e0153ab97ff61eabab4400e6a5264af427ab30cd69f1857/yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293", size = 103646, upload-time = "2026-07-20T02:05:45.895Z" }, - { url = "https://files.pythonhosted.org/packages/74/f0/33b9271c7f881766359d58266fa0811d2e5210ed860e28da7dc6d7786344/yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e", size = 115305, upload-time = "2026-07-20T02:05:47.832Z" }, - { url = "https://files.pythonhosted.org/packages/ef/65/fd79fb1868c4a80db8661091de525bf430f63c3bea1b20e8b6a84fc7d359/yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b", size = 108404, upload-time = "2026-07-20T02:05:49.604Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ba/dbabe6b262f17a816c70cfc09558dbf03ece3ec76684d02f911a3d3a189c/yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce", size = 115940, upload-time = "2026-07-20T02:05:51.741Z" }, - { url = "https://files.pythonhosted.org/packages/a5/43/fab2d1dad9d340a268cdde63756a123d069723efff6a372d123fa74a9517/yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba", size = 110006, upload-time = "2026-07-20T02:05:53.554Z" }, - { url = "https://files.pythonhosted.org/packages/c4/27/41eb51bbd1b8d89546b83897cfb0164f1e109304fd408dbb151b639eec0f/yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b", size = 97618, upload-time = "2026-07-20T02:05:55.57Z" }, - { url = "https://files.pythonhosted.org/packages/3c/25/b2553764b3d65db711d8f45416351ec4f420847558eb669edcbcaadf5780/yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c", size = 93018, upload-time = "2026-07-20T02:05:57.554Z" }, - { url = "https://files.pythonhosted.org/packages/e1/63/64ef361967cc983573149dc1515d531db5da8a4c92d22bb833d59e01b313/yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2", size = 135075, upload-time = "2026-07-20T02:05:59.671Z" }, - { url = "https://files.pythonhosted.org/packages/bb/89/55920fd853ce43e608adbc3962456f0d649d6bb15250dc2988321da0fe1c/yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb", size = 97225, upload-time = "2026-07-20T02:06:01.769Z" }, - { url = "https://files.pythonhosted.org/packages/15/f0/7688d3f2cfff7590df2af38ec46d969f4281a4dddb08a9ad2eafbcdddf98/yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075", size = 96751, upload-time = "2026-07-20T02:06:03.676Z" }, - { url = "https://files.pythonhosted.org/packages/05/1a/a851a0f94aaaf379dd4f901bfc80f634280bec51eb260b47363e2a4cd62e/yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff", size = 107960, upload-time = "2026-07-20T02:06:05.699Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a8/faea066c12f9c77ca0de90641f1655f9dd7b412477bf28c76d692f3aecff/yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448", size = 103500, upload-time = "2026-07-20T02:06:07.556Z" }, - { url = "https://files.pythonhosted.org/packages/fb/9c/1e67084c2a6e2f2db0e3be798328cb3be42c0119b621d25461479a224d21/yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f", size = 115780, upload-time = "2026-07-20T02:06:09.599Z" }, - { url = "https://files.pythonhosted.org/packages/58/86/1f94664e147474337e3359f52012cf3d02f825f694317b178bfba1078c62/yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd", size = 115308, upload-time = "2026-07-20T02:06:11.352Z" }, - { url = "https://files.pythonhosted.org/packages/0a/43/8e55ae7538ba5f28ccb3c845c6dd4549cf7016d5992e5326512519107cdd/yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16", size = 110574, upload-time = "2026-07-20T02:06:13.129Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ba/a889ec8765cedcf2ac44dcb02d6a21e4861399b243b263c5f2dde27ee740/yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213", size = 109914, upload-time = "2026-07-20T02:06:15.243Z" }, - { url = "https://files.pythonhosted.org/packages/9c/c3/e45f821af67b791c2dbbe4a9f4137a1d33f8d386654a05a0c3f47bdfa25d/yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24", size = 107712, upload-time = "2026-07-20T02:06:17.443Z" }, - { url = "https://files.pythonhosted.org/packages/02/00/2ab0f42c9857fcb490bfaa6647b14540b53d241ab209f23220b958cc5832/yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385", size = 104251, upload-time = "2026-07-20T02:06:19.259Z" }, - { url = "https://files.pythonhosted.org/packages/7a/70/709d9a286e98af2c7fd8e4e6cada658b5c0e30d87dd7e2a63c2fb5767217/yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c", size = 115319, upload-time = "2026-07-20T02:06:21.207Z" }, - { url = "https://files.pythonhosted.org/packages/5c/6c/3eaa515142991fe84cfc483ff986492211f1978f90161ccefdbec919d09b/yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4", size = 109163, upload-time = "2026-07-20T02:06:23.006Z" }, - { url = "https://files.pythonhosted.org/packages/bb/64/711dafce66c323a3144d470547a71c5384c57623308ac8bb5e4b903ac148/yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144", size = 115435, upload-time = "2026-07-20T02:06:24.923Z" }, - { url = "https://files.pythonhosted.org/packages/cf/f3/9b9d0e6d84bea851eb1ba99e4bdc755b86fd813e49ec86dfe42f26befdef/yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4", size = 110691, upload-time = "2026-07-20T02:06:26.973Z" }, - { url = "https://files.pythonhosted.org/packages/86/e4/62a06b7e87c4246ac76b7c2da136f972eb4a3a1fc94abb07e7022d6fdb0a/yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740", size = 97454, upload-time = "2026-07-20T02:06:29.163Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c9/5fc8025b318ab10db413b61056bd0d95c557a70e8df4210c7511f866329c/yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1", size = 92813, upload-time = "2026-07-20T02:06:31.113Z" }, - { url = "https://files.pythonhosted.org/packages/a9/08/5f3085fef9564217074db9dd8573de1795bc82cde61a7ad10b6a7234a569/yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76", size = 135680, upload-time = "2026-07-20T02:06:33.273Z" }, - { url = "https://files.pythonhosted.org/packages/98/35/ba9436e579bd48a8801f2021d842d9ab4994c26e4c7dd3a4c1f1bcb57a9e/yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d", size = 97395, upload-time = "2026-07-20T02:06:35.259Z" }, - { url = "https://files.pythonhosted.org/packages/18/a9/a07f76f3c44e02b25cc743af5ef93eef27f7013eadca770451b6a6ccb5db/yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75", size = 97223, upload-time = "2026-07-20T02:06:37.216Z" }, - { url = "https://files.pythonhosted.org/packages/77/f7/a9a1d6fa7dd9e388f95b30f6ad3ec4e285f6c8f61f44ce16070c3fcfe414/yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9", size = 108777, upload-time = "2026-07-20T02:06:39.292Z" }, - { url = "https://files.pythonhosted.org/packages/2f/44/e0b86c302471fabd6f02808ecf2ac52b8412b624787849d4bf2cdb466f6f/yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede", size = 103119, upload-time = "2026-07-20T02:06:41.456Z" }, - { url = "https://files.pythonhosted.org/packages/d1/16/9c16d180bf8faaf223225eb50e1245870ff1ae0e302a27153988e65c51fd/yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca", size = 116471, upload-time = "2026-07-20T02:06:43.696Z" }, - { url = "https://files.pythonhosted.org/packages/d2/8d/b219b9df28a02ce95cfbdd41d2f7caa5669d0ff979c1c9975697145e33c5/yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027", size = 115974, upload-time = "2026-07-20T02:06:45.874Z" }, - { url = "https://files.pythonhosted.org/packages/9b/e8/f20557aca240d88e69850ad1ee91756821d094bb1310565c04d25c6682a2/yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9", size = 110830, upload-time = "2026-07-20T02:06:47.852Z" }, - { url = "https://files.pythonhosted.org/packages/db/18/199b85109a53eeca64ee19c9cca228287e8e4ab0cc1a09b28f530e65cce0/yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41", size = 110054, upload-time = "2026-07-20T02:06:49.84Z" }, - { url = "https://files.pythonhosted.org/packages/aa/2f/ed28147f8cd7f48c49367c90713b30a555284b6105a6a56f3a05568da795/yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373", size = 108312, upload-time = "2026-07-20T02:06:51.835Z" }, - { url = "https://files.pythonhosted.org/packages/c5/c5/55e16ae0a5c227cea8df1c6871ba57d614a34243146c05729caf2a1bd9c5/yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36", size = 103662, upload-time = "2026-07-20T02:06:54.061Z" }, - { url = "https://files.pythonhosted.org/packages/8d/ea/dbd7c2caec459c9a426f18b02688ecbfb58620d0f6a3422d24769fbaf8ab/yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0", size = 116090, upload-time = "2026-07-20T02:06:56.015Z" }, - { url = "https://files.pythonhosted.org/packages/06/84/39ce4ce3059e07fece5fbdbee8c4053406af9aca911ce9fa5f8548aab6af/yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5", size = 109523, upload-time = "2026-07-20T02:06:57.926Z" }, - { url = "https://files.pythonhosted.org/packages/a9/8b/71ff44137b405c64a7788075669c24010019f57a7464b78c3a6cbee539d9/yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5", size = 116084, upload-time = "2026-07-20T02:06:59.868Z" }, - { url = "https://files.pythonhosted.org/packages/62/c0/423078fdd4042e1862c11f0ffd977a0ffa393783c12bee94685923bc189e/yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4", size = 111006, upload-time = "2026-07-20T02:07:01.907Z" }, - { url = "https://files.pythonhosted.org/packages/cf/52/6daa2ee9d95e5c98b8128f8df91eb692eb423ab274b8cf08db52152fad26/yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad", size = 99215, upload-time = "2026-07-20T02:07:03.852Z" }, - { url = "https://files.pythonhosted.org/packages/ec/0e/464a847d7359e0da75dd9fc5c1d1aa35d0159ea31e5f8e66a3c1c29ff3d0/yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f", size = 94566, upload-time = "2026-07-20T02:07:06.074Z" }, - { url = "https://files.pythonhosted.org/packages/e2/55/e03acc4446772660bc335e86e41ef31e4d0d838fd641531a11a5ee33b493/yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88", size = 142533, upload-time = "2026-07-20T02:07:08.284Z" }, - { url = "https://files.pythonhosted.org/packages/ae/71/4acd3a1fc7cf14345cdb302665ecd2097f62c365b4f14ca17d4f37775cf9/yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba", size = 100776, upload-time = "2026-07-20T02:07:10.197Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0b/cfb76b7fe99686db264bff829779a539d923e7564ffd7ef18da6c54c3774/yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928", size = 100913, upload-time = "2026-07-20T02:07:12.357Z" }, - { url = "https://files.pythonhosted.org/packages/8b/3f/7116e782992abbd4fb6948488aec72078895e929a23078290739e8396fce/yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f", size = 106507, upload-time = "2026-07-20T02:07:14.173Z" }, - { url = "https://files.pythonhosted.org/packages/33/90/d4d2d73ee78229cc889872eb8e085d8f5c6f51abdb178409fd9b23cf74fd/yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95", size = 99219, upload-time = "2026-07-20T02:07:16.019Z" }, - { url = "https://files.pythonhosted.org/packages/3e/fa/a6df1a9bccd644eec00abee0dff4277416222cec435330fd1f2858523ec1/yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc", size = 111804, upload-time = "2026-07-20T02:07:18.141Z" }, - { url = "https://files.pythonhosted.org/packages/8a/9e/7b2a1f4bcc20e9447156dd2b1c4d01f70d9df0759025ee7d09a84ffae134/yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da", size = 110943, upload-time = "2026-07-20T02:07:20.06Z" }, - { url = "https://files.pythonhosted.org/packages/08/ff/22c92affb0f9b623ca753d27d968b5625b868f12c6378d049d55ae247643/yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a", size = 108251, upload-time = "2026-07-20T02:07:22.217Z" }, - { url = "https://files.pythonhosted.org/packages/45/44/5769b96298c1e195fb412997b6090af2a84105cf59c17613558a2d011d1f/yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0", size = 106025, upload-time = "2026-07-20T02:07:24.083Z" }, - { url = "https://files.pythonhosted.org/packages/4c/40/009e8e791fd9762c0e1567e69248acb4f49064597e1680874c16dd8bb798/yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498", size = 106573, upload-time = "2026-07-20T02:07:26.248Z" }, - { url = "https://files.pythonhosted.org/packages/20/c6/b7480578f8a0a80946f36ad6df547ecec704f9ba69d2de60f8aa6f1c1cbf/yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104", size = 100751, upload-time = "2026-07-20T02:07:28.098Z" }, - { url = "https://files.pythonhosted.org/packages/d4/27/4476f3360b91a48c5cf125e91f59a3bd35299d84a431a258d57f5977bb11/yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331", size = 111643, upload-time = "2026-07-20T02:07:30.88Z" }, - { url = "https://files.pythonhosted.org/packages/4c/4b/5cdd3e5ee944e8af31e52f6cd3d3af5fd7b937e036ccbbba2c9ffebede95/yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550", size = 106312, upload-time = "2026-07-20T02:07:33.06Z" }, - { url = "https://files.pythonhosted.org/packages/18/86/f406b0c2a6f99575de2da671ef47aa06f89a5be83a27a46971c3b86cecdb/yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6", size = 110379, upload-time = "2026-07-20T02:07:35.155Z" }, - { url = "https://files.pythonhosted.org/packages/f0/6c/9f3adfbd3b30b4fa0f7ccb3a83eba2c1152d3fff554d535e640ba0f7ba2b/yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047", size = 108497, upload-time = "2026-07-20T02:07:37.35Z" }, - { url = "https://files.pythonhosted.org/packages/dd/37/91eb2e5ca883a529c1b390348a74cd9fc0512171727f547ce70bfe02be5c/yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104", size = 102450, upload-time = "2026-07-20T02:07:39.578Z" }, - { url = "https://files.pythonhosted.org/packages/bf/f4/ed5c402ac8fde4403ed3366c2716bfddc8a6677ebd59f3d62772cc7fe468/yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688", size = 97222, upload-time = "2026-07-20T02:07:41.55Z" }, - { url = "https://files.pythonhosted.org/packages/61/02/962c1cbfc401a30c1d034dc67ff395f64b52302c6d62de556c1fca99acc0/yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7", size = 58612, upload-time = "2026-07-20T02:07:43.461Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/75/16/e8be8e2fb175bbf41a0680381a319f1199fae256588241a2ac8677eafb49/yarl-1.25.1.tar.gz", hash = "sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7", size = 246245, upload-time = "2026-09-15T19:35:02.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/b3/cd32ac66ae622b854c2df0ac52106dda220d361b65a64fde7d5b3684aa3f/yarl-1.25.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d7aa6debf92a1dd14cb5280b083a764169a13cfb23a452111160274ed989f4", size = 144798, upload-time = "2026-09-15T19:31:01.821Z" }, + { url = "https://files.pythonhosted.org/packages/61/fb/a2c52a8007c2051ba74662afb112ecf3d00346af4c25e33df9d80fd14fb8/yarl-1.25.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:83d4a37e4b95da4d8bda930d6d35b75b4cdadbacbb4980cae290ea3100b5d51d", size = 104583, upload-time = "2026-09-15T19:31:04.05Z" }, + { url = "https://files.pythonhosted.org/packages/be/dd/ee38aec8e09fdf957e50d4085453fbe202f56c6c3b4cf07b81cdb4f09ee9/yarl-1.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e029648f9c951db30e98a7d7ec90835db88ec4b32820efe2a9bdc2287e032eb6", size = 104325, upload-time = "2026-09-15T19:31:06.338Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b3/058dbfb1857b484c9cf9cc135659f50b85ce66e03c99e44dc2f7b6161f55/yarl-1.25.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d781294bb815ecb5ea57ff6bbf8038e0a31a95fdf3e1788f66e0dc100d64b58", size = 115358, upload-time = "2026-09-15T19:31:08.593Z" }, + { url = "https://files.pythonhosted.org/packages/db/39/29693446cf0cf6b15a0e2f75a5d40f93c56819b05b0622196f45e95b5cc0/yarl-1.25.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e12c538e00e7c1b286a07061046b90e8124e6a9793efae2c70db6a4aad07faad", size = 107658, upload-time = "2026-09-15T19:31:10.802Z" }, + { url = "https://files.pythonhosted.org/packages/86/b3/3c4dd7e1af43b931fba95e0a722737f2ea94a6d199c802585282831d7abd/yarl-1.25.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e4de3ac4adbad3d0bc7c6f4360a7dbff5de2f15e3b723be3198074e17fd9c40", size = 122660, upload-time = "2026-09-15T19:31:12.84Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b5/1b60dbc3cfc9c5712b15148c206748f2bc93953ffdbe25ea75b63dfc89c9/yarl-1.25.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:419f392a1da624877975709e3864dfe833af6cc7671b39318086d456e288380c", size = 126506, upload-time = "2026-09-15T19:31:15.088Z" }, + { url = "https://files.pythonhosted.org/packages/bc/7b/ca212cbe170ac8b96e45317ecbcf9c3c3ecf0cdec98d5b088a9c4088929b/yarl-1.25.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6f117789d22dce188e5754e8bc65b7e6ebf8cb73963b9fa761f672a5883769d", size = 117050, upload-time = "2026-09-15T19:31:17.241Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c3/72b4938cdbe619ad71ac156182faef4908846b84dc3ca4dbb4c4e6f84014/yarl-1.25.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80e47012e730da131c9f059c80936783f9659aae22dc31c03c0595590d11ed54", size = 114174, upload-time = "2026-09-15T19:31:19.294Z" }, + { url = "https://files.pythonhosted.org/packages/e8/43/268717870f9ba0cc9701a95181587f6dc8c5f387aab4aeecc83158f38a79/yarl-1.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e80f557716fd765439577131e526b8942ffc2c07bdbc5e39fa62f660ba1e963f", size = 114944, upload-time = "2026-09-15T19:31:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/da/84/baa5bf504d51fe062c4bcaf62936da97fffb43285978d0b39984824231fd/yarl-1.25.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f61964f235a43738bfac50da46fc4254943a7eea3051aeb0b6fc7c992c29fadc", size = 108263, upload-time = "2026-09-15T19:31:23.388Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/779a2ed9e0152a601a27039bed9aead3f0b79797a67e2c44bfa444622dd8/yarl-1.25.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e546fe1d4a93ebc2910f0d768baff19faa09843ab3f2036a67ed6e69fae4419d", size = 122184, upload-time = "2026-09-15T19:31:25.343Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1f/118e9e5b8f07694d63fd3222e801d7782270003f1a222aa798df3f8d5933/yarl-1.25.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cce0727fd5ac04d372fa9bbfde9febc2bcf209aadfcf0468e45dec72719895d1", size = 114001, upload-time = "2026-09-15T19:31:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/a4cf1cf372313734b17996d4007f9f73596e7a178b9485802e5494ecf484/yarl-1.25.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af4ea5b37403ef4e30f3927eaed540db942bde01d8d3ff083527c0704d1c9c68", size = 120565, upload-time = "2026-09-15T19:31:29.47Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ad94f93ca731bc9e44d321833ab96b82a4f9f5f63cf773f81a4aeea5ecc1/yarl-1.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:68782fdb4027b8d1eee25ec35e9a6db05e863b899eb0310b3a33b6c3fef55707", size = 117060, upload-time = "2026-09-15T19:31:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/bb/cc/51a7b4abf4ac593b8e7eb3794b28e5a35ae26eed8bc04787628d215af82f/yarl-1.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:7d575b54cb3863ef9bc290ea4b009999d55dc237326131e4853cf33e888fee03", size = 102593, upload-time = "2026-09-15T19:31:33.329Z" }, + { url = "https://files.pythonhosted.org/packages/9d/21/0941a6b93a58b59a1ec75e5333bf06929b671309c43c0cd201c172d9c39f/yarl-1.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:bc3ac7bf569f6b64dad04dd7808c7872dae8a97df657856eac05e9b7e3614a85", size = 97697, upload-time = "2026-09-15T19:31:35.855Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/2f3129bbcc9a5c8ba12cc2b29d8060a3bab9c8043c456cfd4b5ca3188890/yarl-1.25.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5", size = 143623, upload-time = "2026-09-15T19:31:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f1bc3390fdca352826676b531d0712736f156919090206700421d46b2c37/yarl-1.25.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26", size = 104011, upload-time = "2026-09-15T19:31:40.25Z" }, + { url = "https://files.pythonhosted.org/packages/a8/aa/50acc5c3e5da04172ae3c281c75405af4d2ca911e16120ab0563f4dffb66/yarl-1.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec", size = 103677, upload-time = "2026-09-15T19:31:42.46Z" }, + { url = "https://files.pythonhosted.org/packages/30/d2/7d1e0ab9f8390e1fbcede5a6dbf70d23c96ad09b8c5567f3a514d1ddb0e2/yarl-1.25.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc", size = 115392, upload-time = "2026-09-15T19:31:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/71/e1/5ba1e3a2a22139213655e760919038e8ed7e2d4a99826d0bbddb3beb96e5/yarl-1.25.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184", size = 107493, upload-time = "2026-09-15T19:31:46.273Z" }, + { url = "https://files.pythonhosted.org/packages/f5/53/780653d5e0f73831f467cf13548912e5eec97f21dc49fc8daf21da027df4/yarl-1.25.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651", size = 122537, upload-time = "2026-09-15T19:31:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/03/92/d54fa70236c6036271c9c9c09fd978df5cbe3ef49ef6c46e9b833476d215/yarl-1.25.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df", size = 126170, upload-time = "2026-09-15T19:31:50.872Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b7/a82a49bf88340b837ef6972b508a1604ae377b9e6904b46b10cf5f1cf925/yarl-1.25.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab", size = 117012, upload-time = "2026-09-15T19:31:53.189Z" }, + { url = "https://files.pythonhosted.org/packages/ef/78/5d684b411e3f3602464ee9b538db48205038f8605872985f61efb809ced0/yarl-1.25.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3", size = 114950, upload-time = "2026-09-15T19:31:55.694Z" }, + { url = "https://files.pythonhosted.org/packages/2f/11/51d82b852c64f7fad0fc7a7ff3031517204887e874c722bbca839c0b23ac/yarl-1.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a", size = 115428, upload-time = "2026-09-15T19:31:57.966Z" }, + { url = "https://files.pythonhosted.org/packages/e4/49/9d1978049bf646b9ea918313926453c6901b71c92f097467777d47d36a88/yarl-1.25.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b", size = 108428, upload-time = "2026-09-15T19:32:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/43/35/7b8f1ebb45d7ec3dda7d1909bf44f458de41ef91e2937f107733582a5166/yarl-1.25.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a", size = 121961, upload-time = "2026-09-15T19:32:02.436Z" }, + { url = "https://files.pythonhosted.org/packages/63/d6/d8b689ab7ca26edeb85f6ff28812aac7a25376eefc1780e303a7bfbaceff/yarl-1.25.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a", size = 114961, upload-time = "2026-09-15T19:32:04.456Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d5/1a1798ea4dc6b7ee3260010a27907ebc697c95dae99817d817ed446d24aa/yarl-1.25.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98", size = 120036, upload-time = "2026-09-15T19:32:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/91/8d/b1b35ed7903da6669b1d367cb2c09436acd4ff508029b4f39a0c0c2058fc/yarl-1.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa", size = 117276, upload-time = "2026-09-15T19:32:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8f/4db01cef62caff0d7a4593ed694fb8a41a27a11158cab80d290221f13e57/yarl-1.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9", size = 101945, upload-time = "2026-09-15T19:32:11.545Z" }, + { url = "https://files.pythonhosted.org/packages/c0/5e/3ce00497c5c0babb74d4130c10c3828ccd215b4819d12020c42429f991ac/yarl-1.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe", size = 97270, upload-time = "2026-09-15T19:32:14.127Z" }, + { url = "https://files.pythonhosted.org/packages/80/cf/54023edfab7aa773b860503db0c56e962ccab0922803ee97988c176ea090/yarl-1.25.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a9ca696eb02e5c02a8afd872ada510eba9b7fe6e68b9572c2e9a9b1941e31e2e", size = 143975, upload-time = "2026-09-15T19:32:16.416Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a8/e6c1be0e6761d0f2d10bbf33a3e1e02b99dc83874d92945d7b461a72481e/yarl-1.25.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a5877f2255aab518ebe528289037699201d5dc5f045f2396cb30aa02db22f57f", size = 104018, upload-time = "2026-09-15T19:32:18.364Z" }, + { url = "https://files.pythonhosted.org/packages/6e/bb/dda344765ffd3430afe1a1c66c866a57fae67786537d4f14607df6505ac1/yarl-1.25.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7a5c3115595995779ee21f2567035793911c3802a43c74f3fbb0314929ec67ac", size = 104156, upload-time = "2026-09-15T19:32:20.459Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5f/ed1538bcd06009fe990d6d283dd7667f639e62a81e35c6d8c6ef6c08fb3c/yarl-1.25.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77e5099b99b37f3cf79c246998ca9f7313a78054cd1809ec46bc1afad47e1c4c", size = 116025, upload-time = "2026-09-15T19:32:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/a2/af/2185daf56b99830d3356ecfada46faaa49945de6626e842b7728088d4980/yarl-1.25.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6efaf45df6a849cef613a03a94c845647456662f85438c886bb67a9c027c8c2c", size = 106985, upload-time = "2026-09-15T19:32:24.749Z" }, + { url = "https://files.pythonhosted.org/packages/c1/65/bc1ae564fb4b04a30b6a8f250e787772581c57e4c3d5cf07ac3359de3103/yarl-1.25.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5f90e44653c4e0f78501ed9bb7d3fce835a8d62b7c6ed0cb16557534087e743", size = 123030, upload-time = "2026-09-15T19:32:27.084Z" }, + { url = "https://files.pythonhosted.org/packages/6a/3e/e2afcde10d74e53b3fa889960991efb3019beda2b1682a01de720a302056/yarl-1.25.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:632da579b2d879f6bad20f2cfa35ded1efe2f4f77f8abb26a6234a5b236acd2f", size = 126765, upload-time = "2026-09-15T19:32:29.332Z" }, + { url = "https://files.pythonhosted.org/packages/a2/be/415b00c0fe5a0615b062a456b26623d7ec91c2bee20faea1a14045aa0469/yarl-1.25.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30eec96e8a91bd588ce897c9543f6d5d8d34b28fbcba28a4dedf20ebeae9fe57", size = 117199, upload-time = "2026-09-15T19:32:31.49Z" }, + { url = "https://files.pythonhosted.org/packages/97/27/3d8c63ddd3e8bcfd033748ab93876678ce59bacd66e4cb1ed851c9c5b37e/yarl-1.25.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:12b6bc4906e11f5e1a1cdcb12296e7afbd366c783cc8073403cd2fb74334e453", size = 115187, upload-time = "2026-09-15T19:32:34.137Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/7a81d0be1a502a26a0d4326c6f2ecb736c824f570ea1c6529f2b0b227b50/yarl-1.25.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9d6ed3d17bccce4c05343e1ca8da13bc5c02c812a4e7282ddd05e8769322d3fc", size = 116085, upload-time = "2026-09-15T19:32:36.438Z" }, + { url = "https://files.pythonhosted.org/packages/f0/69/39fff459916aa0fab42215dc47b759586fd80f94aa56dfc4a7c15ba6e0dc/yarl-1.25.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f38a70074041d3b7e138e452799f5174198bae5bd5ab2000917badf403908c5f", size = 107996, upload-time = "2026-09-15T19:32:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/c0/39/80b9a55a3335590451d9ecf3eb593a8c635351f4c905ef056d7e8a8fd9e7/yarl-1.25.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca89e4e21854ed27ec753297dde84b16c9f8e53b14a4866fb44457d643c19f8", size = 122549, upload-time = "2026-09-15T19:32:41.151Z" }, + { url = "https://files.pythonhosted.org/packages/42/7d/a179c6757818bb59372a4adafd09f7f26a3b4a0f04c3ae404b544c0b0c82/yarl-1.25.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1ab7618921a93767387a4b83776f751588f5b5ae9bb5bc96620e2e2e00bca868", size = 115107, upload-time = "2026-09-15T19:32:43.072Z" }, + { url = "https://files.pythonhosted.org/packages/32/2b/a773ac867e4ab53a98ed98e5cefe3bae31e6f550252ca9d1de266f1a40c5/yarl-1.25.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0ae12ff2b805fa02c4dab838005caef735e39986322698c48588d3beacb65c62", size = 120666, upload-time = "2026-09-15T19:32:45.061Z" }, + { url = "https://files.pythonhosted.org/packages/bc/41/52be6505e85b0f76b4f85b01b5de7e06a0512201abc2c95e14e099549174/yarl-1.25.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90c30ed53546da833c700115c0064c22120d1b1560f474699fd31f22dd668233", size = 117505, upload-time = "2026-09-15T19:32:47.177Z" }, + { url = "https://files.pythonhosted.org/packages/f5/01/349c0386caedbbe488d519f252df54efac8a1459282d466c474bdd84a620/yarl-1.25.1-cp314-cp314-win_amd64.whl", hash = "sha256:acfa7e22aa6c6e7a5996a41d275bfa01efa7ea56ab890590280e9063e2cf5c1b", size = 103446, upload-time = "2026-09-15T19:32:49.615Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f0/8ec63180f77912f0dc4e5a42760cb8c08d20da1d5ace3578a01b84d1f3d8/yarl-1.25.1-cp314-cp314-win_arm64.whl", hash = "sha256:8e7d98cdbb6d71e726f7d525952867096053d1f290dd4e3c50d7d313a136f414", size = 99159, upload-time = "2026-09-15T19:32:51.686Z" }, + { url = "https://files.pythonhosted.org/packages/47/7d/92d2220d6886b70ab1ed8579533ac2af2dfac716d5d929001daff7986df9/yarl-1.25.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d21f0fa80a02d05299207eeaafef345d812ace96d5306e4ef265e1d419a615fa", size = 150071, upload-time = "2026-09-15T19:32:53.911Z" }, + { url = "https://files.pythonhosted.org/packages/64/fc/b245e448124bcda9340df38e3553fa222b50260fca027a84095e9bd8642d/yarl-1.25.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17c9877a89fb6e2bca6f9087eb24cd7fb434653946ef5075e470d23d49b52287", size = 106780, upload-time = "2026-09-15T19:32:56.443Z" }, + { url = "https://files.pythonhosted.org/packages/51/e2/9a6ce2e334ebf218a30335ae76fb1696459430d42f733b8cb0d7d65b84d3/yarl-1.25.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:29273edf1530e397bd07cb784db1fbe0d2590b77569f2e24679a9c0a2d763b94", size = 107361, upload-time = "2026-09-15T19:32:58.827Z" }, + { url = "https://files.pythonhosted.org/packages/ed/70/66e8c76b569b450d16e190f15071c916c3df70b0e33927e415ac497cf0c2/yarl-1.25.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7abffdf37af1cec6a2ad69b827aa84320db5894791bc8ed932dc93fb274b7e9", size = 114396, upload-time = "2026-09-15T19:33:02.24Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/0d82838a05c57fdc05bc8b66e8c92dcc0df15e27463a5f163142d521c682/yarl-1.25.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2239a02249d9326655419e0168a28ca9008938eaab31dc29fc875c217927a6c0", size = 104882, upload-time = "2026-09-15T19:33:04.494Z" }, + { url = "https://files.pythonhosted.org/packages/86/d4/ea08615c4edaa6049a13a2f1128944d068d1893abda7d708d4d7ea01599a/yarl-1.25.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:664ec6a520b74a1df2810666eb67695fcb77fa663e6ea0a25aaf2e529cb24dfa", size = 119485, upload-time = "2026-09-15T19:33:06.583Z" }, + { url = "https://files.pythonhosted.org/packages/1a/82/0898bdce9b1ae403b308b9c733d0d24af4a3464270c2c081f457b16c3e0d/yarl-1.25.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f1c91f5a5980a937ff8e238e98e6897e1ad74a4b1e2c0d68c73b5ffbb3f5c0b", size = 122490, upload-time = "2026-09-15T19:33:08.653Z" }, + { url = "https://files.pythonhosted.org/packages/d1/38/97d79b81c342b78246cfedb74809e68841f3198d21653e10d3232bd9c622/yarl-1.25.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c88edaec8c349ad4c5ad4c486a3defcc4b80ceb2f074436ffa0a87caf5e76a6", size = 115336, upload-time = "2026-09-15T19:33:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9d/2577896554cd310dc470adb6da0b7dd0b435cb63e2565204a7ac240e504c/yarl-1.25.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35dcbea443fafb3eece757ad4e514560ddeb6c34cfae1582c620d7b293d7feee", size = 111825, upload-time = "2026-09-15T19:33:13.204Z" }, + { url = "https://files.pythonhosted.org/packages/29/6b/7ac49d8ba84a5c4bd73415a4c949d22c749cb3762579b3d50e48019a78aa/yarl-1.25.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:882569ff613758cac762a457a5d72d6e211b28d4bcfea89d1d71ea942b02eac0", size = 114655, upload-time = "2026-09-15T19:33:15.553Z" }, + { url = "https://files.pythonhosted.org/packages/e5/18/e5942a16723f5b72f9b1297fd5a85a54f6300cd15c0dcb5005b90cd89156/yarl-1.25.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d0f1489233a254bb3643d2f05de7d59019254d81daeca6b9162fe9edef57e0c7", size = 106395, upload-time = "2026-09-15T19:33:17.599Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2d/549fa46240781513ebc47ae7eb418df428a163a2a3d644cc9cbb3ecb7846/yarl-1.25.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f41753a76f4f63927d03a0d8ba8f5ce0f2083bec29a8cfaccc55371b1564b96b", size = 119277, upload-time = "2026-09-15T19:33:19.973Z" }, + { url = "https://files.pythonhosted.org/packages/76/16/4763f78dcdc0b3b9fb3842b04afe72b9320857c6a69300c62a0eab03d119/yarl-1.25.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fb0eb4955adf0579001581f2f71a126e8781ba61bcd120f127b0401163c6c2d", size = 112504, upload-time = "2026-09-15T19:33:22.464Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b4/974e3edfe0d188393ce1cb9de400111c63fe61f4eb3b772a500d84c970d1/yarl-1.25.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a1e32763e641a1566507d90a8d3b19bfc3cc04a9d4e5ae3e32189874ed4b58a3", size = 116243, upload-time = "2026-09-15T19:33:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/b0/aa/157b940428da80c104ca09666a740e51c94963df65d5b112e06b52e4d7a8/yarl-1.25.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65b5b2066651b7432d389e9799d979c703bcc6ef44266bb8153ef54e91e4aab3", size = 115822, upload-time = "2026-09-15T19:33:26.886Z" }, + { url = "https://files.pythonhosted.org/packages/7e/af/19fbdce41412e1b96825544cc52cd7029d3724655d0988237972f078bd29/yarl-1.25.1-cp314-cp314t-win_amd64.whl", hash = "sha256:734f6e5400352ac4254456003d462866c684703570929cff7a7bde015d0cb371", size = 107386, upload-time = "2026-09-15T19:33:29.009Z" }, + { url = "https://files.pythonhosted.org/packages/2a/99/f6431c8968e89be608d74b28ae2d024521b2953f27dd44e0dece5e04f67a/yarl-1.25.1-cp314-cp314t-win_arm64.whl", hash = "sha256:287e99ff5aa4dc1c7630bfc683ded6f106d756c99dec432a2d7f197a784f51c6", size = 102094, upload-time = "2026-09-15T19:33:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/54/22/318c7980066769c6bcd9221ed2248294f5698811da099013098c670565ed/yarl-1.25.1-py3-none-any.whl", hash = "sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3", size = 63617, upload-time = "2026-09-15T19:34:59.616Z" }, ] [[package]] From 19c60f86b6ea735b28c7b4d259701b28de53b09f Mon Sep 17 00:00:00 2001 From: JR Boos Date: Wed, 16 Sep 2026 09:53:48 -0400 Subject: [PATCH 117/120] fix cves fix more cves iron brain test solution --- .konflux/build-args-konflux.conf | 4 +- .konflux/rpms.in.yaml | 5 +- .konflux/rpms.lock.yaml | 164 ++++++++++++++++++++++++-- deploy/lightspeed-stack/Containerfile | 5 +- 4 files changed, 160 insertions(+), 18 deletions(-) diff --git a/.konflux/build-args-konflux.conf b/.konflux/build-args-konflux.conf index 02915eeb5..23ddf89d2 100644 --- a/.konflux/build-args-konflux.conf +++ b/.konflux/build-args-konflux.conf @@ -1,4 +1,4 @@ -BUILDER_BASE_IMAGE=quay.io/aipcc/base-images/cpu:3.5.1-1787661100 +BUILDER_BASE_IMAGE=quay.io/aipcc/base-images/cpu:3.5.1-1789137687 BUILDER_DNF_COMMAND=dnf -RUNTIME_BASE_IMAGE=quay.io/aipcc/base-images/cpu:3.5.1-1787661100 +RUNTIME_BASE_IMAGE=quay.io/aipcc/base-images/cpu:3.5.1-1789137687 RUNTIME_DNF_COMMAND=dnf diff --git a/.konflux/rpms.in.yaml b/.konflux/rpms.in.yaml index 2fa7d1470..3866b1d54 100644 --- a/.konflux/rpms.in.yaml +++ b/.konflux/rpms.in.yaml @@ -9,9 +9,8 @@ packages: ] upgradePackages: [ - libxslt, - libarchive, - python3-urllib3, + acl, + ffmpeg-free-rhai, ] contentOrigin: repofiles: ["./redhat.repo"] diff --git a/.konflux/rpms.lock.yaml b/.konflux/rpms.lock.yaml index 5cfaa7ad2..039f44652 100644 --- a/.konflux/rpms.lock.yaml +++ b/.konflux/rpms.lock.yaml @@ -4,6 +4,69 @@ lockfileVendor: redhat arches: - arch: aarch64 packages: + - url: https://cdn.redhat.com/content/dist/layered/rhel9/aarch64/rhelai/3.5/os/Packages/f/ffmpeg-free-rhai-6.1.6-9.el9ai.aarch64.rpm + repoid: rhelai-3.5-for-rhel-9-aarch64-rpms + size: 1515025 + checksum: sha256:c6bfee462ad3b3f5b4fd19cf96367d549ea7bfeea0ab4ffe634dd497ba5847ba + name: ffmpeg-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/aarch64/rhelai/3.5/os/Packages/l/libavcodec-free-rhai-6.1.6-9.el9ai.aarch64.rpm + repoid: rhelai-3.5-for-rhel-9-aarch64-rpms + size: 4031909 + checksum: sha256:9ca273eea853bd5c0781add66b1620978468ce9e54a288e972bb668b4d6d7360 + name: libavcodec-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/aarch64/rhelai/3.5/os/Packages/l/libavdevice-free-rhai-6.1.6-9.el9ai.aarch64.rpm + repoid: rhelai-3.5-for-rhel-9-aarch64-rpms + size: 75640 + checksum: sha256:4f32d0216ddc8e6f83732aece446b0e618f0bcd358f85c86fb9d5e8b760c3da7 + name: libavdevice-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/aarch64/rhelai/3.5/os/Packages/l/libavfilter-free-rhai-6.1.6-9.el9ai.aarch64.rpm + repoid: rhelai-3.5-for-rhel-9-aarch64-rpms + size: 1446844 + checksum: sha256:795dd36bacac138d1efb3f1a41e8cb6e9f13843a856fd9f6ef6fe6e9c0b4bf9b + name: libavfilter-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/aarch64/rhelai/3.5/os/Packages/l/libavformat-free-rhai-6.1.6-9.el9ai.aarch64.rpm + repoid: rhelai-3.5-for-rhel-9-aarch64-rpms + size: 1136615 + checksum: sha256:c17182dfc59bda4af4bda6da1efcd13c860c11af50523910e708fd5debfa177c + name: libavformat-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/aarch64/rhelai/3.5/os/Packages/l/libavutil-free-rhai-6.1.6-9.el9ai.aarch64.rpm + repoid: rhelai-3.5-for-rhel-9-aarch64-rpms + size: 345879 + checksum: sha256:00d27c22f8aed4753ef047771597c7f263030e06549d445e22a9206abd1f3664 + name: libavutil-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/aarch64/rhelai/3.5/os/Packages/l/libpostproc-free-rhai-6.1.6-9.el9ai.aarch64.rpm + repoid: rhelai-3.5-for-rhel-9-aarch64-rpms + size: 39547 + checksum: sha256:8b1a8cc49a38014789179fb39eb3bbe81ae74b58da2d078f2d8581461222f03e + name: libpostproc-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/aarch64/rhelai/3.5/os/Packages/l/libswresample-free-rhai-6.1.6-9.el9ai.aarch64.rpm + repoid: rhelai-3.5-for-rhel-9-aarch64-rpms + size: 60307 + checksum: sha256:bb0259b949e925f3eed54e75cde70e76e0992938ee24c82cf1bae0b85a461a00 + name: libswresample-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/aarch64/rhelai/3.5/os/Packages/l/libswscale-free-rhai-6.1.6-9.el9ai.aarch64.rpm + repoid: rhelai-3.5-for-rhel-9-aarch64-rpms + size: 162587 + checksum: sha256:bd4e3882c78fffd4ffac7d99bdcea42d7eebd337a6da75223580eeb828ac55be + name: libswscale-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm - url: https://cdn.redhat.com/content/eus/rhel9/9.6/aarch64/appstream/os/Packages/c/cargo-1.84.1-1.el9.aarch64.rpm repoid: rhel-9-for-aarch64-appstream-eus-rpms__9_DOT_6 size: 7744425 @@ -60,6 +123,13 @@ arches: name: rust-std-static evr: 1.84.1-1.el9 sourcerpm: rust-1.84.1-1.el9.src.rpm + - url: https://cdn.redhat.com/content/eus/rhel9/9.6/aarch64/baseos/os/Packages/a/acl-2.4.0-0.el9_6.1.aarch64.rpm + repoid: rhel-9-for-aarch64-baseos-eus-rpms__9_DOT_6 + size: 77159 + checksum: sha256:3b88acdb403b28e3d9b72010d6794d2306a3a92295e264f9ca6447883886ac8b + name: acl + evr: 2.4.0-0.el9_6.1 + sourcerpm: acl-2.4.0-0.el9_6.1.src.rpm - url: https://cdn.redhat.com/content/eus/rhel9/9.6/aarch64/baseos/os/Packages/e/ed-1.14.2-12.el9.aarch64.rpm repoid: rhel-9-for-aarch64-baseos-eus-rpms__9_DOT_6 size: 78931 @@ -74,17 +144,80 @@ arches: name: info evr: 6.7-15.el9 sourcerpm: texinfo-6.7-15.el9.src.rpm - - url: https://cdn.redhat.com/content/eus/rhel9/9.6/aarch64/baseos/os/Packages/p/python3-urllib3-1.26.5-6.el9_6.2.noarch.rpm + - url: https://cdn.redhat.com/content/eus/rhel9/9.6/aarch64/baseos/os/Packages/l/libacl-2.4.0-0.el9_6.1.aarch64.rpm repoid: rhel-9-for-aarch64-baseos-eus-rpms__9_DOT_6 - size: 223128 - checksum: sha256:177899cd61996a1a459f23508428fe8be31d174cf998c624155e13137a10a4e9 - name: python3-urllib3 - evr: 1.26.5-6.el9_6.2 - sourcerpm: python-urllib3-1.26.5-6.el9_6.2.src.rpm + size: 24904 + checksum: sha256:44a0d95be843d5b5104a6182e13de076c3ed83321977aee6f69148a1d5fb2780 + name: libacl + evr: 2.4.0-0.el9_6.1 + sourcerpm: acl-2.4.0-0.el9_6.1.src.rpm source: [] module_metadata: [] - arch: x86_64 packages: + - url: https://cdn.redhat.com/content/dist/layered/rhel9/x86_64/rhelai/3.5/os/Packages/f/ffmpeg-free-rhai-6.1.6-9.el9ai.x86_64.rpm + repoid: rhelai-3.5-for-rhel-9-x86_64-rpms + size: 1518752 + checksum: sha256:cd24a91037cbbdc29277a574666fc5e49dcfd582afd63f6f6fe0099ea1470c36 + name: ffmpeg-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/x86_64/rhelai/3.5/os/Packages/l/libavcodec-free-rhai-6.1.6-9.el9ai.x86_64.rpm + repoid: rhelai-3.5-for-rhel-9-x86_64-rpms + size: 4213330 + checksum: sha256:7786b455d74f44cc89b7a8017da2d606e3cbfa7f07b64ad575527e583bdbd178 + name: libavcodec-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/x86_64/rhelai/3.5/os/Packages/l/libavdevice-free-rhai-6.1.6-9.el9ai.x86_64.rpm + repoid: rhelai-3.5-for-rhel-9-x86_64-rpms + size: 76627 + checksum: sha256:620c454f720454d13e881966d770e598554b9db1f9e5daeed03a4c87aaf20e98 + name: libavdevice-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/x86_64/rhelai/3.5/os/Packages/l/libavfilter-free-rhai-6.1.6-9.el9ai.x86_64.rpm + repoid: rhelai-3.5-for-rhel-9-x86_64-rpms + size: 1531710 + checksum: sha256:32c1f98752cf9e40d18a6171ab68f7a3f9a5c435ae44e1beb6450ce1af183e9c + name: libavfilter-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/x86_64/rhelai/3.5/os/Packages/l/libavformat-free-rhai-6.1.6-9.el9ai.x86_64.rpm + repoid: rhelai-3.5-for-rhel-9-x86_64-rpms + size: 1122106 + checksum: sha256:aee63758df3f99f5962a5ba61ca07c6c9b5d1ccde4677dd6ddc4c999599cd921 + name: libavformat-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/x86_64/rhelai/3.5/os/Packages/l/libavutil-free-rhai-6.1.6-9.el9ai.x86_64.rpm + repoid: rhelai-3.5-for-rhel-9-x86_64-rpms + size: 351489 + checksum: sha256:3338518a95b0931f570cadea63983faaee1a053e382c6fe7b09cd6e5c629275d + name: libavutil-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/x86_64/rhelai/3.5/os/Packages/l/libpostproc-free-rhai-6.1.6-9.el9ai.x86_64.rpm + repoid: rhelai-3.5-for-rhel-9-x86_64-rpms + size: 46878 + checksum: sha256:31356012fb0d14be447a9357e4bb9be23de095072bdd5654d70efe2643fe1d11 + name: libpostproc-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/x86_64/rhelai/3.5/os/Packages/l/libswresample-free-rhai-6.1.6-9.el9ai.x86_64.rpm + repoid: rhelai-3.5-for-rhel-9-x86_64-rpms + size: 66438 + checksum: sha256:6252004c850c7741fb49e18c5eb86896192051caa6623c96051ac30c6e0c0a70 + name: libswresample-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/x86_64/rhelai/3.5/os/Packages/l/libswscale-free-rhai-6.1.6-9.el9ai.x86_64.rpm + repoid: rhelai-3.5-for-rhel-9-x86_64-rpms + size: 188983 + checksum: sha256:f114a579b7cdc4458baecc516ca93f4abf64a66fa21eda0f186d5ef9799a8640 + name: libswscale-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm - url: https://cdn.redhat.com/content/eus/rhel9/9.6/x86_64/appstream/os/Packages/c/cargo-1.84.1-1.el9.x86_64.rpm repoid: rhel-9-for-x86_64-appstream-eus-rpms__9_DOT_6 size: 8292467 @@ -141,6 +274,13 @@ arches: name: rust-std-static evr: 1.84.1-1.el9 sourcerpm: rust-1.84.1-1.el9.src.rpm + - url: https://cdn.redhat.com/content/eus/rhel9/9.6/x86_64/baseos/os/Packages/a/acl-2.4.0-0.el9_6.1.x86_64.rpm + repoid: rhel-9-for-x86_64-baseos-eus-rpms__9_DOT_6 + size: 77918 + checksum: sha256:7922ff8b02bdd4f0771e3036c3d293939bc2d182505772d7fccfb28a650fedc6 + name: acl + evr: 2.4.0-0.el9_6.1 + sourcerpm: acl-2.4.0-0.el9_6.1.src.rpm - url: https://cdn.redhat.com/content/eus/rhel9/9.6/x86_64/baseos/os/Packages/e/ed-1.14.2-12.el9.x86_64.rpm repoid: rhel-9-for-x86_64-baseos-eus-rpms__9_DOT_6 size: 79993 @@ -155,12 +295,12 @@ arches: name: info evr: 6.7-15.el9 sourcerpm: texinfo-6.7-15.el9.src.rpm - - url: https://cdn.redhat.com/content/eus/rhel9/9.6/x86_64/baseos/os/Packages/p/python3-urllib3-1.26.5-6.el9_6.2.noarch.rpm + - url: https://cdn.redhat.com/content/eus/rhel9/9.6/x86_64/baseos/os/Packages/l/libacl-2.4.0-0.el9_6.1.x86_64.rpm repoid: rhel-9-for-x86_64-baseos-eus-rpms__9_DOT_6 - size: 223128 - checksum: sha256:177899cd61996a1a459f23508428fe8be31d174cf998c624155e13137a10a4e9 - name: python3-urllib3 - evr: 1.26.5-6.el9_6.2 - sourcerpm: python-urllib3-1.26.5-6.el9_6.2.src.rpm + size: 25027 + checksum: sha256:3fefd0c83d5b54dab6f88bd85fd00493983fc26656c5e1f62f5ea56ce1711388 + name: libacl + evr: 2.4.0-0.el9_6.1 + sourcerpm: acl-2.4.0-0.el9_6.1.src.rpm source: [] module_metadata: [] diff --git a/deploy/lightspeed-stack/Containerfile b/deploy/lightspeed-stack/Containerfile index 1ad3c6b2b..37fd828c5 100644 --- a/deploy/lightspeed-stack/Containerfile +++ b/deploy/lightspeed-stack/Containerfile @@ -117,7 +117,10 @@ COPY --from=builder /app-root/LICENSE /licenses/ USER root # Additional tools for derived images -RUN ${RUNTIME_DNF_COMMAND} install -y --nodocs --setopt=keepcache=0 --setopt=tsflags=nodocs jq patch +# Note: --allowerasing is only supported by dnf, not microdnf (used by the +# *-minimal base images), so it is only appended when RUNTIME_DNF_COMMAND=dnf. +RUN ${RUNTIME_DNF_COMMAND} install -y --nodocs --setopt=keepcache=0 --setopt=tsflags=nodocs jq patch && \ + ${RUNTIME_DNF_COMMAND} update -y --nodocs --setopt=keepcache=0 --setopt=tsflags=nodocs # Create OGX directories for library mode RUN mkdir -p /opt/app-root/src/.llama/storage /opt/app-root/src/.llama/providers.d && \ From cec46adf715697ed8d0dcc0b64831e2ae1648983 Mon Sep 17 00:00:00 2001 From: JR Boos Date: Wed, 16 Sep 2026 10:38:49 -0400 Subject: [PATCH 118/120] feat(version): bump version to 0.7.0rc3 --- src/observability/__init__.py | 2 +- src/version.py | 2 +- tests/e2e/features/info.feature | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/observability/__init__.py b/src/observability/__init__.py index 51821a565..a68587915 100644 --- a/src/observability/__init__.py +++ b/src/observability/__init__.py @@ -36,7 +36,7 @@ org_id="12345678", system_id="abc-def-123", request_id="req_xyz789", - cla_version="CLA/0.6.0rc2", + cla_version="CLA/0.7.0rc3", system_os="RHEL", system_version="9.3", system_arch="x86_64", diff --git a/src/version.py b/src/version.py index 029964889..3f3c23f06 100644 --- a/src/version.py +++ b/src/version.py @@ -9,4 +9,4 @@ # [tool.pdm.version] # source = "file" # path = "src/version.py" -__version__ = "0.6.0rc2" +__version__ = "0.7.0rc3" diff --git a/tests/e2e/features/info.feature b/tests/e2e/features/info.feature index 29a5dd0d9..a8afbea11 100644 --- a/tests/e2e/features/info.feature +++ b/tests/e2e/features/info.feature @@ -18,7 +18,7 @@ Feature: Info tests Scenario: Check if info endpoint is working When I access REST API endpoint "info" using HTTP GET method Then The status code of the response is 200 - And The body of the response has proper name Lightspeed Core Service (LCS) and version 0.6.0rc2 + And The body of the response has proper name Lightspeed Core Service (LCS) and version 0.7.0rc3 And The body of the response has ogx version 1.2.5 From 3a695071366fe004b1568dcb906ffda75762df7c Mon Sep 17 00:00:00 2001 From: JR Boos Date: Wed, 16 Sep 2026 10:39:03 -0400 Subject: [PATCH 119/120] docs(version): bump version supported to 0.7.0rc3 --- docs/maintenance/versions_supported.md | 5 ++++- docs/user_doc/splunk.md | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/maintenance/versions_supported.md b/docs/maintenance/versions_supported.md index 151a0cc26..e73000716 100644 --- a/docs/maintenance/versions_supported.md +++ b/docs/maintenance/versions_supported.md @@ -7,5 +7,8 @@ Dates shown in italic are scheduled and can be adjusted. | 0.5.1 | 2026-03-01 | Bugfix | 2026-06-30 | to be decided | Pavel Tišnovský, Erin Bournival, Štefan Bunčiak | | 0.5.2 | 2026-06-30 | Bugfix | TBD | to be decided | Pavel Tišnovský, Erin Bournival, Štefan Bunčiak | | 0.6.0rc2 | 2026-05-01 | Current | 2026-05-28 | to be decided | Pavel Tišnovský, Erin Bournival, Štefan Bunčiak | -| 0.6.0 | 2026-06-01 | Future | | 2026-11-30 | Pavel Tišnovský, Erin Bournival, Štefan Bunčiak | +| 0.6.0 | 2026-06-01 | Current | 2026-07-01 | 2026-11-30 | Pavel Tišnovský, Erin Bournival, Štefan Bunčiak | +| 0.7.0rc1 | 2026-08-01 | Current | 2026-08-07 | to be decided | Pavel Tišnovský, Erin Bournival, Štefan Bunčiak | +| 0.7.0rc2 | 2026-09-01 | Current | 2026-09-03 | to be decided | Pavel Tišnovský, Erin Bournival, Štefan Bunčiak | +| 0.7.0rc3 | 2026-09-01 | Current | 2026-09-16 | to be decided | Pavel Tišnovský, Erin Bournival, Štefan Bunčiak | | 0.7.0 | 2026-07-01 | Future | | 2027-01-31 | Pavel Tišnovský, Erin Bournival, Štefan Bunčiak | diff --git a/docs/user_doc/splunk.md b/docs/user_doc/splunk.md index d78a77154..c09867cfc 100644 --- a/docs/user_doc/splunk.md +++ b/docs/user_doc/splunk.md @@ -85,7 +85,7 @@ Events follow the rlsapi telemetry format for consistency with existing analytic "system_id": "abc-def-123", "total_llm_tokens": 0, "request_id": "req_xyz789", - "cla_version": "CLA/0.5.0", + "cla_version": "CLA/0.7.0rc3", "system_os": "RHEL", "system_version": "9.3", "system_arch": "x86_64" From 285aea38865ef8ede1497d2129b83adbbbb880e9 Mon Sep 17 00:00:00 2001 From: JR Boos Date: Wed, 16 Sep 2026 10:40:35 -0400 Subject: [PATCH 120/120] docs(openapi): bump version to 0.7.0rc3 --- docs/devel_doc/openapi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/devel_doc/openapi.json b/docs/devel_doc/openapi.json index 50454f43c..af5a5fbdc 100644 --- a/docs/devel_doc/openapi.json +++ b/docs/devel_doc/openapi.json @@ -13,7 +13,7 @@ "name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html" }, - "version": "0.6.0rc2" + "version": "0.7.0rc3" }, "servers": [ {