SaaS fixes (#6578)

Co-authored-by: Claude Opus 4.8 <[email protected]>
Co-authored-by: James Brunton <[email protected]>
Co-authored-by: Reece Browne <[email protected]>
Co-authored-by: ConnorYoh <[email protected]>
Co-authored-by: Reece <[email protected]>
Co-authored-by: EthanHealy01 <[email protected]>
Co-authored-by: Ludy <[email protected]>
This commit is contained in:
Anthony Stirling
2026-06-16 16:41:25 +01:00
committed by GitHub
co-authored by Claude Opus 4.8 James Brunton Reece Browne ConnorYoh Reece EthanHealy01 Ludy
parent 96accea984
commit ddf78d11ae
415 changed files with 29552 additions and 5855 deletions
+8 -4
View File
@@ -5,7 +5,7 @@ from pathlib import Path
import pytest
from stirling.config import AppSettings, RagBackend, load_settings
from stirling.config import AppSettings, DocumentsBackend, load_settings
from stirling.services import build_runtime
from stirling.services.runtime import AppRuntime
@@ -23,10 +23,13 @@ def build_app_settings() -> AppSettings:
fast_model_name="test",
smart_model_max_tokens=8192,
fast_model_max_tokens=2048,
rag_backend=RagBackend.SQLITE,
model_max_concurrency=32,
documents_backend=DocumentsBackend.SQLITE,
rag_embedding_model="voyageai:voyage-4",
rag_store_path=Path(":memory:"),
rag_pgvector_dsn="",
documents_sqlite_path=Path(":memory:"),
documents_pgvector_dsn="",
documents_pgvector_pool_min_size=1,
documents_pgvector_pool_max_size=10,
rag_chunk_size=512,
rag_chunk_overlap=64,
rag_default_top_k=5,
@@ -41,6 +44,7 @@ def build_app_settings() -> AppSettings:
contradiction_canonicaliser_batch_size=500,
max_pages=200,
max_characters=200_000,
require_user_id=False,
posthog_enabled=False,
posthog_api_key="",
posthog_host="https://eu.i.posthog.com",
+8 -4
View File
@@ -16,7 +16,7 @@ from fastapi.testclient import TestClient
from stirling.api import app
from stirling.api.dependencies import get_pdf_comment_agent
from stirling.config import AppSettings, RagBackend, load_settings
from stirling.config import AppSettings, DocumentsBackend, load_settings
from stirling.contracts.pdf_comments import (
PdfCommentInstruction,
PdfCommentRequest,
@@ -35,10 +35,13 @@ class StubSettingsProvider:
fast_model_name="test",
smart_model_max_tokens=8192,
fast_model_max_tokens=2048,
rag_backend=RagBackend.SQLITE,
model_max_concurrency=32,
documents_backend=DocumentsBackend.SQLITE,
rag_embedding_model="test-embed",
rag_store_path=Path(":memory:"),
rag_pgvector_dsn="",
documents_sqlite_path=Path(":memory:"),
documents_pgvector_dsn="",
documents_pgvector_pool_min_size=1,
documents_pgvector_pool_max_size=10,
rag_chunk_size=512,
rag_chunk_overlap=64,
rag_default_top_k=5,
@@ -49,6 +52,7 @@ class StubSettingsProvider:
chunked_reasoner_notes_char_budget=250_000,
max_pages=100,
max_characters=100_000,
require_user_id=False,
posthog_enabled=False,
posthog_api_key="",
posthog_host="https://eu.i.posthog.com",
+58
View File
@@ -0,0 +1,58 @@
from __future__ import annotations
import asyncio
import pytest
from pydantic_ai.messages import ModelMessage, ModelResponse, TextPart
from pydantic_ai.models import Model, ModelRequestParameters
from pydantic_ai.settings import ModelSettings
from stirling.services.runtime import ConcurrencyLimitedModel
class TrackingModel(Model):
"""Records the high-water mark of concurrent in-flight requests."""
def __init__(self) -> None:
super().__init__()
self.active = 0
self.max_active = 0
@property
def model_name(self) -> str:
return "tracking"
@property
def system(self) -> str:
return "test"
async def request(
self,
messages: list[ModelMessage],
model_settings: ModelSettings | None,
model_request_parameters: ModelRequestParameters,
) -> ModelResponse:
self.active += 1
self.max_active = max(self.max_active, self.active)
# Yield twice so every gathered task gets a chance to be in flight
# together before any of them completes.
await asyncio.sleep(0)
await asyncio.sleep(0)
self.active -= 1
return ModelResponse(parts=[TextPart(content="ok")])
@pytest.mark.anyio
async def test_shared_semaphore_caps_concurrency_across_models() -> None:
inner = TrackingModel()
semaphore = asyncio.Semaphore(2)
fast = ConcurrencyLimitedModel(inner, semaphore)
smart = ConcurrencyLimitedModel(inner, semaphore)
params = ModelRequestParameters()
await asyncio.gather(
*(fast.request([], None, params) for _ in range(5)),
*(smart.request([], None, params) for _ in range(5)),
)
assert inner.max_active == 2
+20
View File
@@ -214,6 +214,26 @@ def test_pdf_edit_route() -> None:
assert response.json()["outcome"] == "cannot_do"
def test_routes_require_user_id_when_enforced() -> None:
"""With STIRLING_REQUIRE_USER_ID on, an identity-less request is rejected
at the boundary before any handler runs; supplying X-User-Id is accepted.
The other route tests in this module run with the flag off and cover the
identity-less self-hosted path."""
app.dependency_overrides[load_settings] = lambda: build_app_settings().model_copy(update={"require_user_id": True})
try:
anonymous = client.post("/api/v1/pdf/edit", json={"userMessage": "rotate this"})
identified = client.post(
"/api/v1/pdf/edit",
json={"userMessage": "rotate this"},
headers={"X-User-Id": "alice"},
)
finally:
app.dependency_overrides[load_settings] = build_app_settings
assert anonymous.status_code == 401
assert identified.status_code == 200
def test_pdf_questions_route() -> None:
response = client.post(
"/api/v1/pdf/questions",
+8 -4
View File
@@ -77,17 +77,20 @@ def test_pdf_question_answer_defaults_evidence_list() -> None:
def test_app_settings_accepts_model_configuration() -> None:
from pathlib import Path
from stirling.config import RagBackend
from stirling.config import DocumentsBackend
settings = AppSettings(
smart_model_name="claude-sonnet-4-5-20250929",
fast_model_name="claude-haiku-4-5-20251001",
smart_model_max_tokens=8192,
fast_model_max_tokens=2048,
rag_backend=RagBackend.SQLITE,
model_max_concurrency=32,
documents_backend=DocumentsBackend.SQLITE,
rag_embedding_model="voyageai:voyage-4",
rag_store_path=Path(":memory:"),
rag_pgvector_dsn="",
documents_sqlite_path=Path(":memory:"),
documents_pgvector_dsn="",
documents_pgvector_pool_min_size=1,
documents_pgvector_pool_max_size=10,
rag_chunk_size=512,
rag_chunk_overlap=64,
rag_default_top_k=5,
@@ -98,6 +101,7 @@ def test_app_settings_accepts_model_configuration() -> None:
chunked_reasoner_notes_char_budget=250_000,
max_pages=200,
max_characters=200_000,
require_user_id=False,
posthog_enabled=False,
posthog_api_key="",
posthog_host="https://eu.i.posthog.com",