diff --git a/.env.example b/.env.example index 78606c9..e53ab59 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,10 @@ OPENAI_API_KEY= OPENAI_BASE_URL=https://api.openai.com/v1 MODEL_NAME=gpt-4o-mini +# Cost controls for optional LLM enrichment. These bound evidence sent to the provider. +LLM_MAX_LOG_ENTRIES=50 +LLM_MAX_PROMPT_CHARS=12000 + # Docker Compose mounts ./logs to /shared/logs in both app containers. DEMO_SERVICE_LOG_PATH=/shared/logs/demo-service.log diff --git a/README.md b/README.md index ef3ffde..882bf0f 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,8 @@ For security hardening basics, see `docs/07-security.md` and `SECURITY.md`. For secret handling and enforced assistant redaction rules, see `docs/15-secret-handling-and-redaction.md`. +For practical cost optimization controls, see `docs/16-cost-optimization.md`. + Ask the assistant directly: @@ -126,6 +128,7 @@ ai-infra-starter-kit/ 13-kubernetes-incident-debugging.md 14-kubernetes-production-next-steps.md 15-secret-handling-and-redaction.md + 16-cost-optimization.md infra/ # Docker, Kubernetes, and Terraform starter notes k8s/ # kind-first Kubernetes manifests and walkthrough scripts/ # Local traffic and log helper scripts diff --git a/apps/ai-sre-assistant/README.md b/apps/ai-sre-assistant/README.md index 4f00474..10c8bd1 100644 --- a/apps/ai-sre-assistant/README.md +++ b/apps/ai-sre-assistant/README.md @@ -60,11 +60,26 @@ LLM_PROVIDER=openai OPENAI_API_KEY=your_key OPENAI_BASE_URL=https://api.openai.com/v1 MODEL_NAME=your_model +LLM_MAX_LOG_ENTRIES=50 +LLM_MAX_PROMPT_CHARS=12000 DEMO_SERVICE_METRICS_URL=http://localhost:8000/metrics ``` For local Ollama experiments, set `LLM_PROVIDER=ollama` and use an OpenAI-compatible Ollama base URL. +## Cost Controls + +The rule-based analyzer remains the lowest-cost path. Keep `LLM_PROVIDER=none` or set `use_llm=false` on requests when LLM enrichment is not needed. + +When an LLM provider is enabled, the assistant limits provider prompts with: + +- `LLM_MAX_LOG_ENTRIES`: how many recent log records can be included in the prompt. +- `LLM_MAX_PROMPT_CHARS`: maximum assembled user prompt size before the provider request. + +API responses that attempt LLM enrichment include `llm_cost_controls` so callers can see which limits were active. If prompt evidence is truncated, the response includes an `llm_notice`. + +See `../../docs/16-cost-optimization.md` for the Day 3 cost optimization guide. + ## Redaction The assistant replaces obvious sensitive values with `[REDACTED]` before analysis and before data is sent to an optional LLM provider. diff --git a/apps/ai-sre-assistant/app/llm.py b/apps/ai-sre-assistant/app/llm.py index bb986e0..6b275b1 100644 --- a/apps/ai-sre-assistant/app/llm.py +++ b/apps/ai-sre-assistant/app/llm.py @@ -9,12 +9,19 @@ from app.redaction import redact_data, redact_text +DEFAULT_LLM_MAX_LOG_ENTRIES = 50 +DEFAULT_LLM_MAX_PROMPT_CHARS = 12000 +TRUNCATION_MARKER = "\n...[truncated by LLM cost controls]\n" + + @dataclass(frozen=True) class LLMConfig: provider: str api_key: str base_url: str model: str + max_log_entries: int = DEFAULT_LLM_MAX_LOG_ENTRIES + max_prompt_chars: int = DEFAULT_LLM_MAX_PROMPT_CHARS def load_config() -> LLMConfig: @@ -25,6 +32,8 @@ def load_config() -> LLMConfig: api_key=os.getenv("OPENAI_API_KEY", "").strip(), base_url=os.getenv("OPENAI_BASE_URL", default_base_url).strip().rstrip("/"), model=os.getenv("MODEL_NAME", "gpt-4o-mini").strip(), + max_log_entries=_read_positive_int("LLM_MAX_LOG_ENTRIES", DEFAULT_LLM_MAX_LOG_ENTRIES), + max_prompt_chars=_read_positive_int("LLM_MAX_PROMPT_CHARS", DEFAULT_LLM_MAX_PROMPT_CHARS), ) @@ -36,6 +45,13 @@ def is_configured(config: LLMConfig) -> bool: return bool(config.api_key and config.base_url and config.model) +def cost_controls_summary(config: LLMConfig) -> dict[str, int]: + return { + "max_log_entries": config.max_log_entries, + "max_prompt_chars": config.max_prompt_chars, + } + + def analyze_with_llm( question: str, logs: list[dict[str, Any]], @@ -46,22 +62,17 @@ def analyze_with_llm( if not is_configured(config): return None, "LLM provider is not configured; using rule-based analysis." - safe_question = redact_text(question) - safe_logs = redact_data(logs[-50:]) - safe_analysis = redact_data(rule_based_analysis) - evidence = "\n".join(json.dumps(entry, default=str) for entry in safe_logs) - user_prompt = f""" -Question: -{safe_question} - -Rule-based pre-analysis: -{json.dumps(safe_analysis, indent=2, default=str)} - -Recent log evidence: -{evidence} - -{ANALYSIS_INSTRUCTIONS} -""" + user_prompt, prompt_was_truncated = build_user_prompt( + question=question, + logs=logs, + rule_based_analysis=rule_based_analysis, + config=config, + ) + cost_notice = ( + f"LLM prompt was limited to {config.max_prompt_chars} characters by cost controls." + if prompt_was_truncated + else None + ) headers = {"Content-Type": "application/json"} if config.api_key: @@ -82,7 +93,64 @@ def analyze_with_llm( response.raise_for_status() data = response.json() content = data["choices"][0]["message"]["content"] - return redact_text(str(content).strip()), None + return redact_text(str(content).strip()), cost_notice except Exception as exc: - return None, f"LLM request failed; using rule-based analysis. Error: {redact_text(str(exc))}" + failure_notice = f"LLM request failed; using rule-based analysis. Error: {redact_text(str(exc))}" + if cost_notice: + failure_notice = f"{cost_notice} {failure_notice}" + return None, failure_notice + + +def build_user_prompt( + question: str, + logs: list[dict[str, Any]], + rule_based_analysis: dict[str, Any], + config: LLMConfig, +) -> tuple[str, bool]: + safe_question = redact_text(question) + safe_logs = redact_data(logs[-config.max_log_entries :]) + safe_analysis = redact_data(rule_based_analysis) + evidence = "\n".join(json.dumps(entry, default=str) for entry in safe_logs) + + prompt_header = f""" +Question: +{safe_question} + +Rule-based pre-analysis: +{json.dumps(safe_analysis, indent=2, default=str)} + +Recent log evidence: +""" + prompt_footer = f""" + +{ANALYSIS_INSTRUCTIONS} +""" + evidence_budget = config.max_prompt_chars - len(prompt_header) - len(prompt_footer) + if evidence_budget < 0: + prompt, _ = _truncate_text(prompt_header + prompt_footer, config.max_prompt_chars) + return prompt, True + + bounded_evidence, evidence_was_truncated = _truncate_text(evidence, evidence_budget) + return prompt_header + bounded_evidence + prompt_footer, evidence_was_truncated + + +def _read_positive_int(name: str, default: int) -> int: + raw_value = os.getenv(name, "").strip() + if not raw_value: + return default + try: + value = int(raw_value) + except ValueError: + return default + return value if value > 0 else default + + +def _truncate_text(value: str, max_chars: int) -> tuple[str, bool]: + if len(value) <= max_chars: + return value, False + if max_chars <= 0: + return "", True + if max_chars <= len(TRUNCATION_MARKER): + return value[:max_chars], True + return value[: max_chars - len(TRUNCATION_MARKER)].rstrip() + TRUNCATION_MARKER, True diff --git a/apps/ai-sre-assistant/app/main.py b/apps/ai-sre-assistant/app/main.py index 22bec9f..334724f 100644 --- a/apps/ai-sre-assistant/app/main.py +++ b/apps/ai-sre-assistant/app/main.py @@ -4,7 +4,7 @@ from pydantic import BaseModel, Field from app.analyzer import analyze_logs -from app.llm import analyze_with_llm +from app.llm import analyze_with_llm, cost_controls_summary, load_config from app.log_reader import get_log_path, read_recent_logs from app.metrics_analyzer import analyze_metrics, combined_incident_analysis from app.metrics_reader import fetch_metrics_text, get_metrics_url, parse_prometheus_text @@ -89,7 +89,14 @@ def summarize_incident(request: SummarizeIncidentRequest) -> dict[str, Any]: } if request.use_llm: - llm_analysis, llm_notice = analyze_with_llm(question=question, logs=[], rule_based_analysis=response) + llm_config = load_config() + response["llm_cost_controls"] = cost_controls_summary(llm_config) + llm_analysis, llm_notice = analyze_with_llm( + question=question, + logs=[], + rule_based_analysis=response, + config=llm_config, + ) if llm_analysis: response["analysis_mode"] = "llm" response["llm_analysis"] = llm_analysis @@ -114,7 +121,14 @@ def _analyze(question: str, max_lines: int, use_llm: bool) -> dict[str, Any]: } if use_llm: - llm_analysis, llm_notice = analyze_with_llm(question=question, logs=logs, rule_based_analysis=rule_based) + llm_config = load_config() + response["llm_cost_controls"] = cost_controls_summary(llm_config) + llm_analysis, llm_notice = analyze_with_llm( + question=question, + logs=logs, + rule_based_analysis=rule_based, + config=llm_config, + ) if llm_analysis: response["analysis_mode"] = "llm" response["llm_analysis"] = llm_analysis diff --git a/apps/ai-sre-assistant/tests/test_llm_cost_controls.py b/apps/ai-sre-assistant/tests/test_llm_cost_controls.py new file mode 100644 index 0000000..658b697 --- /dev/null +++ b/apps/ai-sre-assistant/tests/test_llm_cost_controls.py @@ -0,0 +1,114 @@ +from fastapi.testclient import TestClient + +from app.llm import LLMConfig, analyze_with_llm, build_user_prompt, cost_controls_summary, load_config +from app.main import app + + +client = TestClient(app) + + +def test_load_config_reads_llm_cost_controls(monkeypatch): + monkeypatch.setenv("LLM_MAX_LOG_ENTRIES", "7") + monkeypatch.setenv("LLM_MAX_PROMPT_CHARS", "2048") + + config = load_config() + + assert cost_controls_summary(config) == { + "max_log_entries": 7, + "max_prompt_chars": 2048, + } + + +def test_llm_prompt_uses_recent_configured_log_window(): + config = LLMConfig( + provider="openai", + api_key="provider-key", + base_url="https://example.invalid/v1", + model="test-model", + max_log_entries=2, + max_prompt_chars=12000, + ) + + prompt, truncated = build_user_prompt( + question="What happened?", + logs=[ + {"message": "oldest-log"}, + {"message": "older-log"}, + {"message": "recent-log"}, + {"message": "newest-log"}, + ], + rule_based_analysis={"summary": "rule-based summary"}, + config=config, + ) + + assert truncated is False + assert "recent-log" in prompt + assert "newest-log" in prompt + assert "oldest-log" not in prompt + assert "older-log" not in prompt + + +def test_llm_prompt_respects_prompt_character_budget(monkeypatch): + captured = {} + + class FakeResponse: + def raise_for_status(self): + return None + + def json(self): + return {"choices": [{"message": {"content": "bounded analysis"}}]} + + class FakeClient: + def __init__(self, timeout): + assert timeout == 30 + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, traceback): + return False + + def post(self, url, headers, json): + captured["prompt"] = json["messages"][1]["content"] + return FakeResponse() + + monkeypatch.setattr("app.llm.httpx.Client", FakeClient) + config = LLMConfig( + provider="openai", + api_key="provider-key", + base_url="https://example.invalid/v1", + model="test-model", + max_log_entries=5, + max_prompt_chars=350, + ) + + result, notice = analyze_with_llm( + question="Investigate the latest failure", + logs=[{"message": "x" * 2000}], + rule_based_analysis={"summary": "y" * 1000}, + config=config, + ) + + assert result == "bounded analysis" + assert notice == "LLM prompt was limited to 350 characters by cost controls." + assert len(captured["prompt"]) <= 350 + + +def test_api_response_reports_llm_cost_controls(tmp_path, monkeypatch): + log_file = tmp_path / "demo-service.log" + log_file.write_text('{"level":"INFO","message":"ok","status_code":200}\n', encoding="utf-8") + monkeypatch.setenv("DEMO_SERVICE_LOG_PATH", str(log_file)) + monkeypatch.setenv("LLM_PROVIDER", "none") + monkeypatch.setenv("LLM_MAX_LOG_ENTRIES", "3") + monkeypatch.setenv("LLM_MAX_PROMPT_CHARS", "4096") + + response = client.post("/ask", json={"question": "What happened?", "use_llm": True}) + + assert response.status_code == 200 + body = response.json() + assert body["analysis_mode"] == "rule-based" + assert body["llm_cost_controls"] == { + "max_log_entries": 3, + "max_prompt_chars": 4096, + } + assert "LLM provider is not configured" in body["llm_notice"] diff --git a/docker-compose.yml b/docker-compose.yml index a4c6d35..c64ebb8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,8 @@ services: OPENAI_API_KEY: ${OPENAI_API_KEY:-} OPENAI_BASE_URL: ${OPENAI_BASE_URL:-https://api.openai.com/v1} MODEL_NAME: ${MODEL_NAME:-gpt-4o-mini} + LLM_MAX_LOG_ENTRIES: ${LLM_MAX_LOG_ENTRIES:-50} + LLM_MAX_PROMPT_CHARS: ${LLM_MAX_PROMPT_CHARS:-12000} DEMO_SERVICE_METRICS_URL: http://demo-service:8000/metrics volumes: - ./logs:/shared/logs diff --git a/docs/07-security.md b/docs/07-security.md index 91cee2d..dddb95e 100644 --- a/docs/07-security.md +++ b/docs/07-security.md @@ -109,6 +109,8 @@ When `LLM_PROVIDER=none`, the assistant uses deterministic rule-based analysis. When a provider is enabled, prompt inputs are redacted immediately before the request. The provider API key is used in the HTTP authorization header and is not included in the prompt. +The assistant also applies cost controls before provider requests: only a bounded number of recent logs are included, and the assembled prompt is capped by `LLM_MAX_PROMPT_CHARS`. This reduces both spend and unnecessary data exposure. + Before enabling an external LLM provider: - review what logs may be sent. diff --git a/docs/11-kubernetes-config-and-secrets.md b/docs/11-kubernetes-config-and-secrets.md index b0376be..5caadac 100644 --- a/docs/11-kubernetes-config-and-secrets.md +++ b/docs/11-kubernetes-config-and-secrets.md @@ -34,6 +34,8 @@ Current values: | `LLM_PROVIDER` | Controls whether the assistant uses rule-based analysis, OpenAI-compatible APIs, or Ollama. | | `OPENAI_BASE_URL` | Stores the OpenAI-compatible API base URL. | | `MODEL_NAME` | Stores the model name used when an LLM provider is enabled. | +| `LLM_MAX_LOG_ENTRIES` | Bounds how many recent log records can enter an optional LLM prompt. | +| `LLM_MAX_PROMPT_CHARS` | Caps the assembled prompt size before an optional provider request. | Inspect the ConfigMap: @@ -41,7 +43,7 @@ Inspect the ConfigMap: kubectl describe configmap ai-infra-starter-kit-config -n ai-infra-starter-kit ``` -The default `LLM_PROVIDER` is `none` so the project works without an API key. +The default `LLM_PROVIDER` is `none` so the project works without an API key. The default cost controls keep provider prompts bounded when a user enables LLM enrichment. ## What Belongs In A Secret diff --git a/docs/15-secret-handling-and-redaction.md b/docs/15-secret-handling-and-redaction.md index 321a602..84692b3 100644 --- a/docs/15-secret-handling-and-redaction.md +++ b/docs/15-secret-handling-and-redaction.md @@ -62,7 +62,7 @@ Before enabling an external provider: - review the log fields and retention policy. - verify that the data is allowed to leave the environment. - review the provider's current data handling and retention terms. -- send the minimum useful number of log lines. +- send the minimum useful number of log lines with `LLM_MAX_LOG_ENTRIES`. - use a scoped, rotatable credential. - treat `[REDACTED]` as a signal that context was intentionally removed; do not ask the model to reconstruct it. @@ -99,4 +99,4 @@ cd apps/ai-sre-assistant python -m pytest -q ``` -See `07-security.md` for the broader threat model and `../SECURITY.md` for vulnerability reporting. +See `07-security.md` for the broader threat model, `16-cost-optimization.md` for prompt budget controls, and `../SECURITY.md` for vulnerability reporting. diff --git a/docs/16-cost-optimization.md b/docs/16-cost-optimization.md new file mode 100644 index 0000000..99ace84 --- /dev/null +++ b/docs/16-cost-optimization.md @@ -0,0 +1,131 @@ +# Cost Optimization Basics + +Week 4, Day 3 adds practical cost controls for the AI SRE Assistant. + +The goal is not to build a billing platform. The goal is to make the first cost drivers visible and controllable before the project introduces heavier production tooling. + +## Cost Model + +In this project, the cheapest path is the default path: + +```text +logs / metrics -> rule-based analysis -> response +``` + +That path runs locally and does not require a paid LLM provider. + +When an OpenAI-compatible provider is enabled, the flow changes: + +```text +logs / metrics -> rule-based analysis -> bounded LLM prompt -> provider response +``` + +At that point, cost is mostly shaped by: + +- whether the assistant calls an LLM at all. +- which model is configured. +- how many log entries are included in the prompt. +- how large the prompt becomes after evidence is serialized. +- how often callers request LLM enrichment. +- provider retries, failures, and latency. + +## Defaults + +The default `.env.example` keeps provider calls disabled: + +```text +LLM_PROVIDER=none +``` + +That means the assistant uses deterministic rule-based analysis unless a user explicitly enables a provider. + +When provider calls are enabled, the assistant applies two budget controls: + +```text +LLM_MAX_LOG_ENTRIES=50 +LLM_MAX_PROMPT_CHARS=12000 +``` + +`LLM_MAX_LOG_ENTRIES` limits how many recent log records can be included in the LLM prompt. + +`LLM_MAX_PROMPT_CHARS` caps the assembled user prompt before the provider request. If the evidence is too large, the assistant truncates it and returns an `llm_notice` explaining that cost controls limited the prompt. + +These are character limits, not exact token limits. They are intentionally simple because the project supports any OpenAI-compatible provider and should remain easy to run locally. + +## Request-Level Controls + +Every log analysis endpoint already supports `use_llm`. + +For the lowest-cost path, set it to `false`: + +```bash +curl -s -X POST http://localhost:8001/ask \ + -H "Content-Type: application/json" \ + -d '{"question":"What failed recently?","max_lines":120,"use_llm":false}' +``` + +For LLM-enriched analysis, leave `use_llm` enabled and configure a provider: + +```bash +curl -s -X POST http://localhost:8001/ask \ + -H "Content-Type: application/json" \ + -d '{"question":"What failed recently?","max_lines":120,"use_llm":true}' +``` + +When `use_llm=true`, the API response includes `llm_cost_controls` so callers can see the prompt limits that were active for that request. + +## Why This Matters + +AI infrastructure cost can grow in places that are easy to miss. + +A single incident summary can be cheap. Repeating that summary on every refresh, sending hundreds of log lines, using a large model by default, or retrying failed provider calls can turn the same workflow into a larger bill. + +The first production-minded habit is to make the call boundary explicit: + +- Do we need an LLM for this request? +- Is the rule-based answer good enough? +- Are we sending only the evidence needed for the question? +- Are prompt limits visible to the caller? +- Are failures falling back safely instead of retrying blindly? + +## Practical Checklist + +For local learning: + +- keep `LLM_PROVIDER=none` unless testing provider behavior. +- use `use_llm=false` for repeatable smoke tests and demos. +- keep `MODEL_NAME` on a small or inexpensive model when experimenting. +- lower `LLM_MAX_LOG_ENTRIES` when logs are noisy. +- lower `LLM_MAX_PROMPT_CHARS` when latency or cost matters more than detail. + +For production planning: + +- track request counts by endpoint and caller. +- track provider calls separately from local rule-based responses. +- record prompt and completion token usage when the provider returns it. +- add rate limits and quotas before exposing the assistant broadly. +- consider caching repeated incident summaries. +- use model routing only after the simple default path is measured. +- alert on unexpected provider call volume. + +## Relationship To Security + +Cost controls and security controls overlap. + +Sending less evidence to an external provider reduces spend, latency, and data exposure. Redaction remains the backup control, but the stronger pattern is to avoid sending unnecessary data in the first place. + +Day 2 redaction answers: what if sensitive data appears in evidence? + +Day 3 cost controls answer: how much evidence should leave the local system at all? + +## Current Limits + +The current implementation is intentionally lightweight: + +- prompt limits use characters, not exact model tokenizer counts. +- no provider-specific pricing table is included. +- no billing dashboard is included. +- no cache is included yet. +- no per-user quota system is included yet. + +Those are good future steps, but they are easier to explain after the basic call boundary and prompt budget are visible. diff --git a/docs/build-log.md b/docs/build-log.md index eec03f4..2328d7f 100644 --- a/docs/build-log.md +++ b/docs/build-log.md @@ -464,3 +464,42 @@ What comes next: - Continue Week 4 with practical cost optimization habits. - Keep assistant evaluation basics next so safety and usefulness can be tested together. + +## Week 4, Day 3 - Cost Optimization Basics + +Today I added the first practical cost controls for the AI SRE Assistant. + +Day 1 named the security risks. Day 2 enforced redaction. Day 3 focuses on a different production habit: make optional LLM usage explicit, bounded, and easy to turn off. + +What changed: + +- Kept the deterministic rule-based analyzer as the default no-cost path. +- Added `LLM_MAX_LOG_ENTRIES` to limit how many recent logs can enter an LLM prompt. +- Added `LLM_MAX_PROMPT_CHARS` to cap the assembled provider prompt. +- Added prompt truncation notices when cost controls reduce evidence size. +- Added `llm_cost_controls` metadata to API responses that attempt LLM enrichment. +- Wired the defaults through `.env.example`, Docker Compose, and the Kubernetes ConfigMap. +- Added tests for environment parsing, log-window limits, prompt-size limits, and API cost-control metadata. +- Added a dedicated cost optimization guide and linked it from the README and assistant docs. +- Updated the security guide to connect cost controls with reduced provider data exposure. + +Why this matters: + +AI infrastructure cost can grow before a billing dashboard exists. A single incident summary may be cheap, but repeated provider calls, large log windows, large prompts, expensive models, and broad refresh loops can turn the same workflow into a cost problem. + +The safest beginner default is still local rule-based analysis. When LLM enrichment is useful, the assistant now sends a bounded amount of evidence and tells callers which limits were active. + +Lessons learned: + +- Cost optimization starts at the call boundary. +- The cheapest provider request is the one you do not need to make. +- A smaller prompt usually means lower cost, lower latency, and lower exposure. +- Character limits are not exact token accounting, but they are useful as beginner guardrails. +- Cost controls should be visible in API responses, not hidden in configuration. +- Security and cost optimization often point in the same direction: send less unnecessary data. + +What comes next: + +- Add assistant evaluation basics so usefulness, safety, and cost can be checked together. +- Consider provider usage metadata when a provider returns token counts. +- Keep dashboards, caching, quotas, and model routing as later steps after the simple controls are understood. diff --git a/infra/k8s/ai-sre-assistant.yaml b/infra/k8s/ai-sre-assistant.yaml index e3e654d..630e1d6 100644 --- a/infra/k8s/ai-sre-assistant.yaml +++ b/infra/k8s/ai-sre-assistant.yaml @@ -42,6 +42,16 @@ spec: configMapKeyRef: name: ai-infra-starter-kit-config key: MODEL_NAME + - name: LLM_MAX_LOG_ENTRIES + valueFrom: + configMapKeyRef: + name: ai-infra-starter-kit-config + key: LLM_MAX_LOG_ENTRIES + - name: LLM_MAX_PROMPT_CHARS + valueFrom: + configMapKeyRef: + name: ai-infra-starter-kit-config + key: LLM_MAX_PROMPT_CHARS - name: DEMO_SERVICE_LOG_PATH valueFrom: configMapKeyRef: diff --git a/infra/k8s/configmap.yaml b/infra/k8s/configmap.yaml index b215c93..e6d3b91 100644 --- a/infra/k8s/configmap.yaml +++ b/infra/k8s/configmap.yaml @@ -14,3 +14,6 @@ data: LLM_PROVIDER: none OPENAI_BASE_URL: https://api.openai.com/v1 MODEL_NAME: gpt-4o-mini + # Bound optional provider prompt size so local experiments stay predictable. + LLM_MAX_LOG_ENTRIES: "50" + LLM_MAX_PROMPT_CHARS: "12000"