feat(mcp-docugen): Task 7-10 renderer, http_routes, mcp_tools, main bootstrap
- Renderer: orchestratore generate() — validazione strict variabili,
materializzazione image vars come asset effimeri su disco + URL rewrite,
asset paths template da ./assets/X -> {PUBLIC_BASE_URL}/assets/<t>/X,
integrazione LLM error -> record success=0
- FastAPI sub-app: GET /health (no auth), /assets/{t}/{f} (auth+traversal check),
/generated/{gen_id}/{f} (410 su scaduto o mancante)
- FastMCP server con 6 tool: template_create/update/delete/list/get,
document_generate. Tools esposti anche via mcp.tools dict per test.
- main.build_app() compone http_app + FastMCP mount su /mcp + auth middleware
+ lifespan cleanup task TTL (24h). run() entry point per script console.
68 test passed. Build Docker arca-mcp-docugen:dev verificata,
/health endpoint risponde correttamente nel container.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import base64
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import respx
|
||||
|
||||
from mcp_docugen.main import build_app
|
||||
from mcp_docugen.models import TemplateFrontmatter, TemplateVariable
|
||||
|
||||
|
||||
def _openrouter_success(text: str = "# Generated"):
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"id": "g",
|
||||
"choices": [{"message": {"role": "assistant", "content": text}}],
|
||||
"model": "anthropic/claude-sonnet-4",
|
||||
"usage": {
|
||||
"prompt_tokens": 50,
|
||||
"completion_tokens": 100,
|
||||
"total_cost": 0.02,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def app_env(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("API_KEY", "test-api-key")
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-test")
|
||||
monkeypatch.setenv("PUBLIC_BASE_URL", "https://mcp.test.com")
|
||||
monkeypatch.setenv("DATA_DIR", str(tmp_path))
|
||||
app = await build_app()
|
||||
return app, tmp_path
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_full_generation_flow_records_and_serves_asset(app_env):
|
||||
app, tmp_path = app_env
|
||||
respx.post("https://openrouter.ai/api/v1/chat/completions").mock(
|
||||
return_value=_openrouter_success("# Done")
|
||||
)
|
||||
|
||||
template_store = app.state.template_store
|
||||
renderer = app.state.renderer
|
||||
generation_store = app.state.generation_store
|
||||
|
||||
fm = TemplateFrontmatter(
|
||||
name="report",
|
||||
description="x",
|
||||
required_variables=[TemplateVariable(name="foto", type="image")],
|
||||
)
|
||||
await template_store.create(
|
||||
name="report", frontmatter=fm, body=""
|
||||
)
|
||||
|
||||
png = b"\x89PNG\r\n\x1a\n" + b"\x00" * 50
|
||||
img_var = {
|
||||
"kind": "image",
|
||||
"data_b64": base64.b64encode(png).decode(),
|
||||
"mime": "image/png",
|
||||
}
|
||||
|
||||
result = await renderer.generate(
|
||||
template_name="report",
|
||||
content_md="content",
|
||||
variables={"foto": img_var},
|
||||
instructions=None,
|
||||
)
|
||||
|
||||
assert result.markdown == "# Done"
|
||||
stats = await generation_store.get_stats()
|
||||
assert stats["success"] == 1
|
||||
|
||||
gen_files = list((tmp_path / "generated" / result.generation_id).iterdir())
|
||||
assert len(gen_files) == 1
|
||||
@@ -0,0 +1,122 @@
|
||||
import base64
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from mcp_docugen.auth import ApiKeyAuthMiddleware
|
||||
from mcp_docugen.generation_store import EphemeralAssetRecord, GenerationStore
|
||||
from mcp_docugen.http_routes import build_http_app
|
||||
from mcp_docugen.models import TemplateFrontmatter
|
||||
from mcp_docugen.template_store import TemplateStore
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def env(tmp_path):
|
||||
template_store = TemplateStore(base_dir=tmp_path / "templates")
|
||||
generation_store = GenerationStore(
|
||||
db_path=tmp_path / "gen.db",
|
||||
generated_dir=tmp_path / "generated",
|
||||
)
|
||||
await generation_store.init()
|
||||
|
||||
inner = build_http_app(template_store, generation_store)
|
||||
app = FastAPI()
|
||||
app.add_middleware(
|
||||
ApiKeyAuthMiddleware, api_key="test-key", exempt_paths={"/health"}
|
||||
)
|
||||
app.mount("/", inner)
|
||||
return TestClient(app), template_store, generation_store, tmp_path
|
||||
|
||||
|
||||
def _headers():
|
||||
return {"Authorization": "Bearer test-key"}
|
||||
|
||||
|
||||
def test_health_no_auth(env):
|
||||
tc, *_ = env
|
||||
r = tc.get("/health")
|
||||
assert r.status_code == 200
|
||||
assert r.json() == {"status": "ok"}
|
||||
|
||||
|
||||
async def test_assets_serve_existing_file(env):
|
||||
tc, template_store, _, _ = env
|
||||
fm = TemplateFrontmatter(name="brand", description="x")
|
||||
assets = [
|
||||
{
|
||||
"filename": "logo.png",
|
||||
"data_b64": base64.b64encode(b"\x89PNG").decode(),
|
||||
"mime": "image/png",
|
||||
}
|
||||
]
|
||||
await template_store.create(
|
||||
name="brand", frontmatter=fm, body="b", assets=assets
|
||||
)
|
||||
|
||||
r = tc.get("/assets/brand/logo.png", headers=_headers())
|
||||
assert r.status_code == 200
|
||||
assert r.content == b"\x89PNG"
|
||||
|
||||
|
||||
def test_assets_require_auth(env):
|
||||
tc, *_ = env
|
||||
r = tc.get("/assets/brand/logo.png")
|
||||
assert r.status_code == 401
|
||||
|
||||
|
||||
def test_assets_path_traversal_rejected(env):
|
||||
tc, *_ = env
|
||||
# Starlette decodifica %2F in / prima del routing, quindi la path
|
||||
# non matcha /assets/{template}/{filename} e torna 404. 400/404 entrambi safe.
|
||||
r = tc.get("/assets/brand/..%2Fevil.png", headers=_headers())
|
||||
assert r.status_code in (400, 404)
|
||||
|
||||
|
||||
def test_assets_missing_returns_404(env):
|
||||
tc, *_ = env
|
||||
r = tc.get("/assets/brand/missing.png", headers=_headers())
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
async def test_generated_fresh_returns_file(env):
|
||||
tc, _, generation_store, tmp_path = env
|
||||
gen_dir = tmp_path / "generated" / "g-1"
|
||||
gen_dir.mkdir(parents=True)
|
||||
(gen_dir / "foto.png").write_bytes(b"image-bytes")
|
||||
await generation_store.register_ephemeral_asset(
|
||||
EphemeralAssetRecord(
|
||||
generation_id="g-1",
|
||||
var_name="foto",
|
||||
file_path=str(gen_dir / "foto.png"),
|
||||
mime="image/png",
|
||||
ttl_days=30,
|
||||
)
|
||||
)
|
||||
r = tc.get("/generated/g-1/foto.png", headers=_headers())
|
||||
assert r.status_code == 200
|
||||
assert r.content == b"image-bytes"
|
||||
|
||||
|
||||
async def test_generated_expired_returns_410(env):
|
||||
tc, _, generation_store, tmp_path = env
|
||||
gen_dir = tmp_path / "generated" / "g-old"
|
||||
gen_dir.mkdir(parents=True)
|
||||
(gen_dir / "foto.png").write_bytes(b"x")
|
||||
await generation_store.register_ephemeral_asset(
|
||||
EphemeralAssetRecord(
|
||||
generation_id="g-old",
|
||||
var_name="foto",
|
||||
file_path=str(gen_dir / "foto.png"),
|
||||
mime="image/png",
|
||||
ttl_days=-1,
|
||||
)
|
||||
)
|
||||
r = tc.get("/generated/g-old/foto.png", headers=_headers())
|
||||
assert r.status_code == 410
|
||||
|
||||
|
||||
def test_generated_unknown_returns_410(env):
|
||||
tc, *_ = env
|
||||
r = tc.get("/generated/unknown-id/file.png", headers=_headers())
|
||||
assert r.status_code == 410
|
||||
@@ -0,0 +1,144 @@
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from mcp_docugen.generation_store import GenerationStore
|
||||
from mcp_docugen.llm_client import LLMResponse
|
||||
from mcp_docugen.mcp_tools import build_mcp_server
|
||||
from mcp_docugen.renderer import Renderer
|
||||
from mcp_docugen.template_store import TemplateStore
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def mcp_env(tmp_path):
|
||||
template_store = TemplateStore(base_dir=tmp_path / "templates")
|
||||
generation_store = GenerationStore(
|
||||
db_path=tmp_path / "gen.db",
|
||||
generated_dir=tmp_path / "generated",
|
||||
)
|
||||
await generation_store.init()
|
||||
llm = AsyncMock()
|
||||
llm.chat.return_value = LLMResponse(
|
||||
text="# Out",
|
||||
model="m",
|
||||
tokens_in=5,
|
||||
tokens_out=10,
|
||||
cost_usd=0.01,
|
||||
latency_ms=50,
|
||||
)
|
||||
renderer = Renderer(
|
||||
template_store=template_store,
|
||||
generation_store=generation_store,
|
||||
llm=llm,
|
||||
public_base_url="https://mcp.example.com",
|
||||
default_model="m",
|
||||
asset_ttl_days=30,
|
||||
max_image_size_mb=10,
|
||||
)
|
||||
mcp = build_mcp_server(template_store, renderer)
|
||||
return mcp, template_store
|
||||
|
||||
|
||||
async def test_template_create_and_get(mcp_env):
|
||||
mcp, _ = mcp_env
|
||||
await mcp.tools["template_create"](
|
||||
name="demo",
|
||||
frontmatter={"name": "demo", "description": "d"},
|
||||
body="body",
|
||||
assets=None,
|
||||
)
|
||||
got = await mcp.tools["template_get"](name="demo")
|
||||
assert got["name"] == "demo"
|
||||
assert got["body"] == "body"
|
||||
|
||||
|
||||
async def test_template_create_duplicate_errors(mcp_env):
|
||||
mcp, _ = mcp_env
|
||||
await mcp.tools["template_create"](
|
||||
name="demo",
|
||||
frontmatter={"name": "demo", "description": "x"},
|
||||
body="b",
|
||||
assets=None,
|
||||
)
|
||||
with pytest.raises(Exception):
|
||||
await mcp.tools["template_create"](
|
||||
name="demo",
|
||||
frontmatter={"name": "demo", "description": "x"},
|
||||
body="b",
|
||||
assets=None,
|
||||
)
|
||||
|
||||
|
||||
async def test_template_list_returns_summaries(mcp_env):
|
||||
mcp, _ = mcp_env
|
||||
await mcp.tools["template_create"](
|
||||
name="a",
|
||||
frontmatter={"name": "a", "description": "A"},
|
||||
body="b",
|
||||
assets=None,
|
||||
)
|
||||
await mcp.tools["template_create"](
|
||||
name="b",
|
||||
frontmatter={"name": "b", "description": "B"},
|
||||
body="b",
|
||||
assets=None,
|
||||
)
|
||||
result = await mcp.tools["template_list"]()
|
||||
names = sorted(t["name"] for t in result)
|
||||
assert names == ["a", "b"]
|
||||
|
||||
|
||||
async def test_template_update(mcp_env):
|
||||
mcp, _ = mcp_env
|
||||
await mcp.tools["template_create"](
|
||||
name="u",
|
||||
frontmatter={"name": "u", "description": "d"},
|
||||
body="old",
|
||||
assets=None,
|
||||
)
|
||||
await mcp.tools["template_update"](
|
||||
name="u",
|
||||
frontmatter={"name": "u", "description": "new"},
|
||||
body="new",
|
||||
assets=None,
|
||||
)
|
||||
got = await mcp.tools["template_get"](name="u")
|
||||
assert got["body"] == "new"
|
||||
assert got["frontmatter"]["description"] == "new"
|
||||
|
||||
|
||||
async def test_template_delete(mcp_env):
|
||||
mcp, _ = mcp_env
|
||||
await mcp.tools["template_create"](
|
||||
name="d",
|
||||
frontmatter={"name": "d", "description": "x"},
|
||||
body="b",
|
||||
assets=None,
|
||||
)
|
||||
await mcp.tools["template_delete"](name="d")
|
||||
with pytest.raises(Exception):
|
||||
await mcp.tools["template_get"](name="d")
|
||||
|
||||
|
||||
async def test_document_generate_happy_path(mcp_env):
|
||||
mcp, _ = mcp_env
|
||||
await mcp.tools["template_create"](
|
||||
name="g",
|
||||
frontmatter={
|
||||
"name": "g",
|
||||
"description": "x",
|
||||
"required_variables": [{"name": "cliente", "type": "string"}],
|
||||
},
|
||||
body="Cliente: {{cliente}}",
|
||||
assets=None,
|
||||
)
|
||||
result = await mcp.tools["document_generate"](
|
||||
template_name="g",
|
||||
content_md="# Ordine",
|
||||
variables={"cliente": "ACME"},
|
||||
instructions=None,
|
||||
)
|
||||
assert result["markdown"] == "# Out"
|
||||
assert result["model"] == "m"
|
||||
assert result["tokens"]["input"] == 5
|
||||
assert "generation_id" in result
|
||||
@@ -0,0 +1,274 @@
|
||||
import base64
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from mcp_docugen.generation_store import GenerationStore
|
||||
from mcp_docugen.llm_client import LLMResponse, OpenRouterClient
|
||||
from mcp_docugen.models import TemplateFrontmatter, TemplateVariable
|
||||
from mcp_docugen.renderer import (
|
||||
ImageTooLarge,
|
||||
InvalidImageEncoding,
|
||||
InvalidVariableType,
|
||||
MissingVariables,
|
||||
Renderer,
|
||||
)
|
||||
from mcp_docugen.template_store import TemplateStore
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def env(tmp_path):
|
||||
template_store = TemplateStore(base_dir=tmp_path / "templates")
|
||||
generation_store = GenerationStore(
|
||||
db_path=tmp_path / "gen.db",
|
||||
generated_dir=tmp_path / "generated",
|
||||
)
|
||||
await generation_store.init()
|
||||
llm = AsyncMock(spec=OpenRouterClient)
|
||||
llm.chat.return_value = LLMResponse(
|
||||
text="# Output",
|
||||
model="anthropic/claude-sonnet-4",
|
||||
tokens_in=10,
|
||||
tokens_out=20,
|
||||
cost_usd=0.001,
|
||||
latency_ms=100,
|
||||
)
|
||||
renderer = Renderer(
|
||||
template_store=template_store,
|
||||
generation_store=generation_store,
|
||||
llm=llm,
|
||||
public_base_url="https://mcp.example.com",
|
||||
default_model="anthropic/claude-sonnet-4",
|
||||
asset_ttl_days=30,
|
||||
max_image_size_mb=10,
|
||||
)
|
||||
fm = TemplateFrontmatter(
|
||||
name="fattura",
|
||||
description="x",
|
||||
required_variables=[TemplateVariable(name="cliente", type="string")],
|
||||
)
|
||||
await template_store.create(
|
||||
name="fattura", frontmatter=fm, body="Cliente: {{cliente}}"
|
||||
)
|
||||
return renderer, llm
|
||||
|
||||
|
||||
async def test_generate_happy_path_string_var(env):
|
||||
renderer, llm = env
|
||||
result = await renderer.generate(
|
||||
template_name="fattura",
|
||||
content_md="# Ordine",
|
||||
variables={"cliente": "ACME"},
|
||||
instructions=None,
|
||||
)
|
||||
assert result.markdown == "# Output"
|
||||
assert result.model == "anthropic/claude-sonnet-4"
|
||||
assert result.tokens.input == 10
|
||||
call_kwargs = llm.chat.await_args.kwargs
|
||||
assert "ACME" in call_kwargs["user"]
|
||||
assert "Cliente: ACME" in call_kwargs["system"]
|
||||
|
||||
|
||||
async def test_generate_missing_required_variable_raises(env):
|
||||
renderer, _ = env
|
||||
with pytest.raises(MissingVariables) as exc:
|
||||
await renderer.generate(
|
||||
template_name="fattura",
|
||||
content_md="x",
|
||||
variables={},
|
||||
instructions=None,
|
||||
)
|
||||
assert "cliente" in str(exc.value)
|
||||
|
||||
|
||||
async def test_generate_wrong_type_raises(env):
|
||||
renderer, _ = env
|
||||
with pytest.raises(InvalidVariableType):
|
||||
await renderer.generate(
|
||||
template_name="fattura",
|
||||
content_md="x",
|
||||
variables={
|
||||
"cliente": {
|
||||
"kind": "image",
|
||||
"data_b64": "x",
|
||||
"mime": "image/png",
|
||||
}
|
||||
},
|
||||
instructions=None,
|
||||
)
|
||||
|
||||
|
||||
async def test_generate_image_variable_is_saved_and_rewritten(tmp_path):
|
||||
template_store = TemplateStore(base_dir=tmp_path / "templates")
|
||||
generation_store = GenerationStore(
|
||||
db_path=tmp_path / "gen.db",
|
||||
generated_dir=tmp_path / "generated",
|
||||
)
|
||||
await generation_store.init()
|
||||
llm = AsyncMock(spec=OpenRouterClient)
|
||||
llm.chat.return_value = LLMResponse(
|
||||
text="# OK", model="m", tokens_in=1, tokens_out=1, cost_usd=0, latency_ms=10
|
||||
)
|
||||
renderer = Renderer(
|
||||
template_store=template_store,
|
||||
generation_store=generation_store,
|
||||
llm=llm,
|
||||
public_base_url="https://mcp.example.com",
|
||||
default_model="m",
|
||||
asset_ttl_days=30,
|
||||
max_image_size_mb=10,
|
||||
)
|
||||
fm = TemplateFrontmatter(
|
||||
name="report",
|
||||
description="x",
|
||||
required_variables=[TemplateVariable(name="foto", type="image")],
|
||||
)
|
||||
await template_store.create(name="report", frontmatter=fm, body="")
|
||||
|
||||
png = b"\x89PNG\r\n\x1a\n" + b"\x00" * 100
|
||||
img_var = {
|
||||
"kind": "image",
|
||||
"data_b64": base64.b64encode(png).decode(),
|
||||
"mime": "image/png",
|
||||
}
|
||||
result = await renderer.generate(
|
||||
template_name="report",
|
||||
content_md="content",
|
||||
variables={"foto": img_var},
|
||||
instructions=None,
|
||||
)
|
||||
|
||||
assert result.ephemeral_assets_urls
|
||||
url = result.ephemeral_assets_urls[0]
|
||||
assert url.startswith("https://mcp.example.com/generated/")
|
||||
assert url.endswith(".png")
|
||||
gen_dir = tmp_path / "generated" / result.generation_id
|
||||
assert gen_dir.exists()
|
||||
assert any(gen_dir.iterdir())
|
||||
|
||||
call_kwargs = llm.chat.await_args.kwargs
|
||||
assert "https://mcp.example.com/generated/" in call_kwargs["system"]
|
||||
|
||||
|
||||
async def test_image_too_large_raises(env):
|
||||
renderer, _ = env
|
||||
big = b"x" * (11 * 1024 * 1024)
|
||||
img_var = {
|
||||
"kind": "image",
|
||||
"data_b64": base64.b64encode(big).decode(),
|
||||
"mime": "image/png",
|
||||
}
|
||||
fm = TemplateFrontmatter(
|
||||
name="big",
|
||||
description="x",
|
||||
required_variables=[TemplateVariable(name="foto", type="image")],
|
||||
)
|
||||
await renderer.template_store.create(name="big", frontmatter=fm, body="{{foto}}")
|
||||
with pytest.raises(ImageTooLarge):
|
||||
await renderer.generate(
|
||||
template_name="big",
|
||||
content_md="x",
|
||||
variables={"foto": img_var},
|
||||
instructions=None,
|
||||
)
|
||||
|
||||
|
||||
async def test_invalid_base64_raises(env):
|
||||
renderer, _ = env
|
||||
fm = TemplateFrontmatter(
|
||||
name="bad",
|
||||
description="x",
|
||||
required_variables=[TemplateVariable(name="foto", type="image")],
|
||||
)
|
||||
await renderer.template_store.create(name="bad", frontmatter=fm, body="{{foto}}")
|
||||
with pytest.raises(InvalidImageEncoding):
|
||||
await renderer.generate(
|
||||
template_name="bad",
|
||||
content_md="x",
|
||||
variables={
|
||||
"foto": {
|
||||
"kind": "image",
|
||||
"data_b64": "!!!not-base64!!!",
|
||||
"mime": "image/png",
|
||||
}
|
||||
},
|
||||
instructions=None,
|
||||
)
|
||||
|
||||
|
||||
async def test_template_asset_paths_are_rewritten(tmp_path):
|
||||
template_store = TemplateStore(base_dir=tmp_path / "templates")
|
||||
generation_store = GenerationStore(
|
||||
db_path=tmp_path / "gen.db",
|
||||
generated_dir=tmp_path / "generated",
|
||||
)
|
||||
await generation_store.init()
|
||||
llm = AsyncMock(spec=OpenRouterClient)
|
||||
llm.chat.return_value = LLMResponse(
|
||||
text="out", model="m", tokens_in=1, tokens_out=1, cost_usd=0, latency_ms=10
|
||||
)
|
||||
renderer = Renderer(
|
||||
template_store=template_store,
|
||||
generation_store=generation_store,
|
||||
llm=llm,
|
||||
public_base_url="https://mcp.example.com",
|
||||
default_model="m",
|
||||
asset_ttl_days=30,
|
||||
max_image_size_mb=10,
|
||||
)
|
||||
fm = TemplateFrontmatter(name="brand", description="x")
|
||||
assets = [
|
||||
{
|
||||
"filename": "logo.png",
|
||||
"data_b64": base64.b64encode(b"\x89PNG").decode(),
|
||||
"mime": "image/png",
|
||||
}
|
||||
]
|
||||
await template_store.create(
|
||||
name="brand",
|
||||
frontmatter=fm,
|
||||
body="Header  footer",
|
||||
assets=assets,
|
||||
)
|
||||
await renderer.generate(
|
||||
template_name="brand",
|
||||
content_md="x",
|
||||
variables={},
|
||||
instructions=None,
|
||||
)
|
||||
system_prompt = llm.chat.await_args.kwargs["system"]
|
||||
assert "https://mcp.example.com/assets/brand/logo.png" in system_prompt
|
||||
assert "./assets/logo.png" not in system_prompt
|
||||
|
||||
|
||||
async def test_generate_uses_frontmatter_model_override(tmp_path):
|
||||
template_store = TemplateStore(base_dir=tmp_path / "templates")
|
||||
generation_store = GenerationStore(
|
||||
db_path=tmp_path / "gen.db",
|
||||
generated_dir=tmp_path / "generated",
|
||||
)
|
||||
await generation_store.init()
|
||||
llm = AsyncMock(spec=OpenRouterClient)
|
||||
llm.chat.return_value = LLMResponse(
|
||||
text="out",
|
||||
model="openai/gpt-4o",
|
||||
tokens_in=1,
|
||||
tokens_out=1,
|
||||
cost_usd=0,
|
||||
latency_ms=10,
|
||||
)
|
||||
renderer = Renderer(
|
||||
template_store=template_store,
|
||||
generation_store=generation_store,
|
||||
llm=llm,
|
||||
public_base_url="https://mcp.example.com",
|
||||
default_model="anthropic/claude-sonnet-4",
|
||||
asset_ttl_days=30,
|
||||
max_image_size_mb=10,
|
||||
)
|
||||
fm = TemplateFrontmatter(name="x", description="y", model="openai/gpt-4o")
|
||||
await template_store.create(name="x", frontmatter=fm, body="b")
|
||||
await renderer.generate(
|
||||
template_name="x", content_md="c", variables={}, instructions=None
|
||||
)
|
||||
assert llm.chat.await_args.kwargs["model"] == "openai/gpt-4o"
|
||||
Reference in New Issue
Block a user