fix: OpenRouter 402 — send max_tokens cost cap; make session window configurable
gates / consistency-and-conventions (push) Successful in 32s
gates / consistency-and-conventions (push) Successful in 32s
User hit 'API error 402: ... You requested up to 131072 tokens, but can only afford 4511' on the assist alias: no provider ever sent max_tokens, so OpenRouter's credit pre-check billed the routed model's full worst-case output; user also asked to bound session history to the last 5 requests/responses. Architect decisions: - AI_MAX_TOKENS (num, default 2048): sent as max_tokens on OpenRouter and generationConfig.maxOutputTokens on Gemini — a real per-request cost ceiling. llamacpp unchanged (local/free, no pre-check). - AI_SESSION_TURNS (num, default 40 kept back-compat; messages, 2 per exchange — 10 = last 5 conversations): resolved lazily in session_push because config loads after the hardcoded line-25 default. - Both registered in the bin/pos-ai POS_CONFIG @General section, so they appear in 'pos config ai' with num: validation. Reviewer hardening (CHANGES_REQUIRED -> fixed): unguarded env input could reach jq tonumber (0/-5/010/abc all savable via config-ui's ^-?[0-9]+$) and abort the CLI; both providers and session_push now guard with ^[1-9][0-9]*$ and fall back to the default. Verified: fake-curl shim smoke (16 provider-body + 12 session-window checks incl. the 010-regression proof), make gen idempotent, make check OK, make lint 0 FAIL/0 WARN, make test 17 files / 299 checks / 0 fail (~49s), bash -n clean, git diff --check clean. Reviewer ACCEPT (twice). Tester regression round (permanent provider-body + session-pruning coverage) intentionally not run this cycle — user's call; remains a documented follow-up.
This commit is contained in:
@@ -42,6 +42,8 @@ summary (newest last).
|
|||||||
|
|
||||||
## Done
|
## Done
|
||||||
|
|
||||||
|
- **2026-09-06** — `pos ai` OpenRouter 402 + unbounded session — Architect→Builder→Reviewer. User hit OpenRouter 402 on the `assist` alias: "You requested up to 131072 tokens, but can only afford 4511" — no provider sent `max_tokens`, so OpenRouter pre-bills the routed model's full worst-case output (131072 on `openrouter/auto`); user also asked to bound session history to last-5 requests. Architect decisions: `AI_MAX_TOKENS` (num, default **2048**, real cost cap) sent as `max_tokens` on OpenRouter and `generationConfig.maxOutputTokens` on Gemini (llamacpp skipped — local/free); `AI_SESSION_TURNS` (messages, 2 per exchange; default 40 kept back-compat; 10 = last 5 conversations) resolved lazily in `session_push` because config loads after the hardcoded line-25 default; both registered in the `bin/pos-ai` `# POS_CONFIG:` `@General` section → visible in `pos config ai`. Reviewer hardening (CHANGES_REQUIRED → fixed): unguarded env input could reach jq `tonumber` (0/-5/010/abc) — both providers + `session_push` now guard with `^[1-9][0-9]*$` fallback-to-default. Verified: fake-curl shim smoke (16 provider-body + 12 session-window checks incl. 010-regression proof), `make gen` idempotent, `make check` OK, `make lint` 0/0, `make test` 17 files/299 checks green; Reviewer ACCEPT (twice). Tester regression round not run this cycle (user's call); permanent coverage remains a follow-up.
|
||||||
|
|
||||||
- **2026-09-06** — `pos ai alias` create-flow silent abort + bogus step labels. User hit: pressing Enter on "System prompt (empty = use built-in)" silently returned to the menu (no alias created); step counters showed `[1/4] [2/4]` in a 5-step flow. Detective: `menu_ask_value` (lib/menu-lib.sh) contract returns rc 1 for empty+no-default, collapsing "empty" with "cancel"; the Step 4 call at `:410` passed `""` default so the advertised empty answer triggered `|| return 0` → silent abort; same latent trap at alias-name `:353` (re-prompt dead code). Pre-existing (introduced with the alias feature `f61766b0`/`9f289ba3`/`300b742a`), NOT a 2026-09-06 regression; 11 other `menu_ask_value` callers correct (6 external rely on empty=cancel, 4 pass defaults) → no global semantic change allowed. Architect: opt-in `--allow-empty` flag on `menu_ask_value` (backward-compatible; empty+no-default → rc 0 empty value, genuine cancel/EOF → rc 1, default wins) + step labels fixed to `/5`. Builder: implemented (lib/menu-lib.sh flag+docs, bin/pos-ai-alias `:353`/`:410` + step counters), 7-case smoke matrix PASS, gen idempotent, check OK, lint 0 FAIL/0 WARN. User chose to commit without the Tester regression round (report: `AgentsReport/tester/2026-09-06_alias-menu-tests.md` — pty feasibility proven, steps 2-6 pending).
|
- **2026-09-06** — `pos ai alias` create-flow silent abort + bogus step labels. User hit: pressing Enter on "System prompt (empty = use built-in)" silently returned to the menu (no alias created); step counters showed `[1/4] [2/4]` in a 5-step flow. Detective: `menu_ask_value` (lib/menu-lib.sh) contract returns rc 1 for empty+no-default, collapsing "empty" with "cancel"; the Step 4 call at `:410` passed `""` default so the advertised empty answer triggered `|| return 0` → silent abort; same latent trap at alias-name `:353` (re-prompt dead code). Pre-existing (introduced with the alias feature `f61766b0`/`9f289ba3`/`300b742a`), NOT a 2026-09-06 regression; 11 other `menu_ask_value` callers correct (6 external rely on empty=cancel, 4 pass defaults) → no global semantic change allowed. Architect: opt-in `--allow-empty` flag on `menu_ask_value` (backward-compatible; empty+no-default → rc 0 empty value, genuine cancel/EOF → rc 1, default wins) + step labels fixed to `/5`. Builder: implemented (lib/menu-lib.sh flag+docs, bin/pos-ai-alias `:353`/`:410` + step counters), 7-case smoke matrix PASS, gen idempotent, check OK, lint 0 FAIL/0 WARN. User chose to commit without the Tester regression round (report: `AgentsReport/tester/2026-09-06_alias-menu-tests.md` — pty feasibility proven, steps 2-6 pending).
|
||||||
|
|
||||||
- **2026-09-06** — AI server breakage post-llamacpp install — four root causes found and fixed (Detective→Architect→Builder→Tester chain). (1) `llama-server --version` prints to STDERR — `detect_llama_version`'s `2>/dev/null` swallowed it → "installed llama.cpp unknown"; (2) `printf|grep -q` under `pipefail` → SIGPIPE rc=141 race randomly rejecting valid flags from the 59 KB `--help`; (3) `resolve_model` expected flat files but the HF downloader creates `<models>/<repo>/file.gguf` dirs → `Model not found`; (4) llama.cpp default port 8080 vs tool's 8088. Architect DQ1-DQ6: help-gated validation stays; dir-expansion never silently picks; `ensure_user_bus` in `lib/common.sh` pre-flights all three tools; `--no-unit` direct-run escape hatch for SSH/headless; candidates narrowed to `llama-server`/`llama-server-cuda`; port pinned 8088; installer post-install sanity. Builder F1-F7 (stderr version capture, pipefail-safe flag validation, model dir expansion, port hardcoding, `--no-unit`, user-bus pre-flight, installer sanity + F1 regex edge: `build 1.2.3`→`1` misparse). Tester: 4 new regression files (version-from-stderr, 20× flag-validation determinism, model dir expansion, bus pre-flight + E2E) + 3 fixture updates; suite now 16 files / 269 checks. Verified: `make gen` idempotent, `make check` OK, `make lint` 0 FAIL / 0 WARN, `make test` 269/269 (~49 s), `bash -n` clean, `git diff --check` clean. Post-fix: user's machine needed only `export XDG_RUNTIME_DIR=/run/user/1000` (linger already on) → Option A systemd-managed server works, or `--no-unit` for direct run.
|
- **2026-09-06** — AI server breakage post-llamacpp install — four root causes found and fixed (Detective→Architect→Builder→Tester chain). (1) `llama-server --version` prints to STDERR — `detect_llama_version`'s `2>/dev/null` swallowed it → "installed llama.cpp unknown"; (2) `printf|grep -q` under `pipefail` → SIGPIPE rc=141 race randomly rejecting valid flags from the 59 KB `--help`; (3) `resolve_model` expected flat files but the HF downloader creates `<models>/<repo>/file.gguf` dirs → `Model not found`; (4) llama.cpp default port 8080 vs tool's 8088. Architect DQ1-DQ6: help-gated validation stays; dir-expansion never silently picks; `ensure_user_bus` in `lib/common.sh` pre-flights all three tools; `--no-unit` direct-run escape hatch for SSH/headless; candidates narrowed to `llama-server`/`llama-server-cuda`; port pinned 8088; installer post-install sanity. Builder F1-F7 (stderr version capture, pipefail-safe flag validation, model dir expansion, port hardcoding, `--no-unit`, user-bus pre-flight, installer sanity + F1 regex edge: `build 1.2.3`→`1` misparse). Tester: 4 new regression files (version-from-stderr, 20× flag-validation determinism, model dir expansion, bus pre-flight + E2E) + 3 fixture updates; suite now 16 files / 269 checks. Verified: `make gen` idempotent, `make check` OK, `make lint` 0 FAIL / 0 WARN, `make test` 269/269 (~49 s), `bash -n` clean, `git diff --check` clean. Post-fix: user's machine needed only `export XDG_RUNTIME_DIR=/run/user/1000` (linger already on) → Option A systemd-managed server works, or `--no-unit` for direct run.
|
||||||
|
|||||||
@@ -0,0 +1,307 @@
|
|||||||
|
# AI cost & session-window design — OpenRouter 402 + bounded session memory
|
||||||
|
|
||||||
|
**Date:** 2026-09-06
|
||||||
|
**Author:** Architect (big-pickle)
|
||||||
|
**HEAD:** 8ce5479 (clean tree)
|
||||||
|
**Mode:** design-only (no file edits besides this report)
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
Two tightly associated defects in `pos ai` / `bin/pos-ai`:
|
||||||
|
|
||||||
|
1. **402 root cause** — no provider ever sends `max_tokens`, so OpenRouter's credit
|
||||||
|
pre-check charges the routed model's full worst-case output (131072 on
|
||||||
|
`openrouter/auto`) and rejects balances under that. Fix: send an explicit capped
|
||||||
|
`max_tokens`.
|
||||||
|
2. **Unbounded-ish session** — `MAX_SESSION_TURNS=40` counts *messages* (20
|
||||||
|
exchanges); user wants "last 5 req/response" = 10 messages.
|
||||||
|
|
||||||
|
**Decisions:** add `AI_MAX_TOKENS` (num, default **2048**) honored by openrouter
|
||||||
|
(body `max_tokens`) and gemini (`generationConfig.maxOutputTokens`); **skip** llamacpp.
|
||||||
|
Add `AI_SESSION_TURNS` (num, default **40** — backward compatible) honored lazily in
|
||||||
|
`session_push`; user sets **10** for 5 pairs. Both vars declared in the `@General`
|
||||||
|
section of the `# POS_CONFIG:` header, docs updated. No chat/alias wrapper changes.
|
||||||
|
|
||||||
|
**Open items:** none blocking. Tester should add provider-body + session-pruning
|
||||||
|
coverage (see §Testing).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verified fact confirmation (A–D)
|
||||||
|
|
||||||
|
All user-reported facts confirmed against source at HEAD:
|
||||||
|
|
||||||
|
**A — 402 root cause: CONFIRMED.**
|
||||||
|
- `lib/ai-providers/openrouter.sh:22-23` — body is only `{model,messages}`; no
|
||||||
|
`max_tokens`. `:24-29` POSTs straight to OpenRouter with unchanged body.
|
||||||
|
- `lib/ai-providers/gemini.sh:17-19` — body only `{contents,...}`; no
|
||||||
|
`generationConfig`. (`:20-23` adds only `systemInstruction`.)
|
||||||
|
- `lib/ai-providers/llamacpp.sh:32-33` — body `{model,messages,stream:false}`.
|
||||||
|
- Conclusion: none carry a generation cap → OpenRouter 402 with the user's thin
|
||||||
|
balance. Fix is to send an explicit `max_tokens`.
|
||||||
|
|
||||||
|
**B — session window: CONFIRMED.**
|
||||||
|
- `bin/pos-ai:25` `MAX_SESSION_TURNS=40`.
|
||||||
|
- `session_push()` `bin/pos-ai:285-290` → `'.messages |= .[-"$MAX_SESSION_TURNS":]'`
|
||||||
|
prunes to last N **messages** (40 msgs = 20 exchanges). User wants 5 pairs = 10 messages.
|
||||||
|
|
||||||
|
**C — config surface: CONFIRMED.**
|
||||||
|
- `# POS_CONFIG:` header `bin/pos-ai:6`, scope `ai | ai.env`.
|
||||||
|
- `num:` type already used in the same header (`LLAMACPP_CTX_SIZE=num:…`,
|
||||||
|
`LLAMACPP_GPU_LAYERS=num:…`).
|
||||||
|
- Validation: `lib/config-ui.sh:419` `*,num,*) [[ "$val" =~ ^-?[0-9]+$ ]]` — integer-only
|
||||||
|
on *entry*; empty input ="kept current value" (`:468-471`); `-` = clear (`:472-477`).
|
||||||
|
So `num:` + empty/unset → falls back to code default. Clean.
|
||||||
|
- Env precedence: `load_env_file` (`lib/config-ui.sh:336-357`) exports a file key only
|
||||||
|
when the variable is not already set in the environment (`:351-353`) → env beats
|
||||||
|
file beats default. Providers read config via env (`AI_API_KEY` pattern).
|
||||||
|
- `PROVIDER_CONFIG` headers exist on `openrouter.sh:7-8`, `gemini.sh:7-8`,
|
||||||
|
`llamacpp.sh:7` for *provider-specific* keys.
|
||||||
|
|
||||||
|
**D — provider resolution: CONFIRMED.**
|
||||||
|
- `bin/pos-ai:679-683` — `--provider` flag > `AI_PROVIDER` env > `gemini`.
|
||||||
|
- `load_config()` `bin/pos-ai:141-151` loads `ai.env` (+ legacy files); called by
|
||||||
|
`resolve_key` (line 155) and at provider resolution (line 681).
|
||||||
|
- Call site `bin/pos-ai:522` (ask) and `:557` (chat): `provider_generate "$model" "$messages" "$system"`.
|
||||||
|
|
||||||
|
**Gates/tests scan:**
|
||||||
|
- No test pins `MAX_SESSION_TURNS`, the provider request bodies, or header text
|
||||||
|
(`t-config-precedence.sh` targets `pos-ai-server`, a separate tool). No forced
|
||||||
|
test update.
|
||||||
|
- `DOC/POS.md` has a hand-maintained ai.env config table (`:88-97`) and mentions
|
||||||
|
"capped at 40 turns" at `:64`. `DOC/HOWTO.md:45` and `DOC/AGENT_Context_Project.md:491`
|
||||||
|
list ai.env vars (hand-maintained). All need doc rows/bumps for the new vars.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision 1: `AI_MAX_TOKENS` — cap generation tokens
|
||||||
|
|
||||||
|
**Status: [DECIDED]**
|
||||||
|
|
||||||
|
### Options & trade-offs
|
||||||
|
|
||||||
|
**Option 1 (chosen) — single global `AI_MAX_TOKENS=num`, default 2048, honored by remote providers (openrouter + gemini); skip llamacpp.**
|
||||||
|
- *Advantages:* smallest change that fixes the 402 (OpenRouter pre-check sees a
|
||||||
|
capped cost) and is a real per-request cost ceiling; one var, one default; fits
|
||||||
|
the existing `AI_*` env naming and the tool-level `@General` config section; no
|
||||||
|
new per-provider surface.
|
||||||
|
- *Costs:* remote providers share one ceiling (no per-provider cap without user
|
||||||
|
intervention).
|
||||||
|
- *Risks:* a too-low cap truncates long answers — mitigated by default 2048 being
|
||||||
|
ample for terse `ask`/`chat` CLI answers; user can raise it.
|
||||||
|
- *Reasoning for default 2048:* conservative (user's balance affords ~4511 tokens at
|
||||||
|
routed price, so 2048 passes the pre-check with margin) while being a practical,
|
||||||
|
real ceiling. 2048 tokens ≈ several thousand chars — plenty for the terse,
|
||||||
|
commands-first assistant role this tool plays.
|
||||||
|
|
||||||
|
**Option 2 — per-provider caps via `PROVIDER_CONFIG` (e.g. `AI_MAX_TOKENS` on openrouter.sh, gemini.sh).**
|
||||||
|
- *Advantages:* independent ceilings per provider.
|
||||||
|
- *Costs:* two declarations, redundant section plumbing, and the *default* (which is
|
||||||
|
the entire point) still has no shared home → awkward. `PROVIDER_CONFIG` is for
|
||||||
|
provider-specific concerns; a cost ceiling + 402 pre-check over both remote
|
||||||
|
providers is tool-level, not provider-specific.
|
||||||
|
|
||||||
|
**Option 3 — no gemini cap; only openrouter.**
|
||||||
|
- *Advantages:* minimal (402 only affects OpenRouter).
|
||||||
|
- *Costs:* leaves Gemini without any cost ceiling while introducing the same var —
|
||||||
|
inconsistent, and Gemini's own pricing can surprise. Rejected.
|
||||||
|
|
||||||
|
**Option 4 — include llamacpp too (`max_tokens` in body; it accepts it).**
|
||||||
|
- *Advantages:* provider parity on the OpenAI-compatible endpoint.
|
||||||
|
- *Costs:* local & free — no credit pre-check, no cost. Adds surface with zero user
|
||||||
|
benefit. Rejected on the "smallest sufficient design" principle.
|
||||||
|
|
||||||
|
### Implementation contract
|
||||||
|
- Env var name: **`AI_MAX_TOKENS`**, type `num`, default **2048**.
|
||||||
|
- OpenRouter body (`openrouter.sh:22-23`): add `max_tokens`.
|
||||||
|
- Gemini body (`gemini.sh:17-19`): add `generationConfig.maxOutputTokens`.
|
||||||
|
- llamacpp: **no change**.
|
||||||
|
- Providers read `"${AI_MAX_TOKENS:-2048}"` from env; value is present because
|
||||||
|
`load_config` runs before `provider_generate` (resolve path confirmed in D).
|
||||||
|
- Declare in `@General` section of `# POS_CONFIG:` header (`bin/pos-ai:6`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision 2: `AI_SESSION_TURNS` — bounded session window
|
||||||
|
|
||||||
|
**Status: [DECIDED]**
|
||||||
|
|
||||||
|
### Options & trade-offs
|
||||||
|
|
||||||
|
**Option 1 (chosen) — new `AI_SESSION_TURNS=num`, default 40 (unchanged), resolved lazily in `session_push`.**
|
||||||
|
- *Advantages:* fully backward compatible — no silent memory truncation for existing
|
||||||
|
users. The user's "last 5 req/response" = setting `AI_SESSION_TURNS=10`. One var,
|
||||||
|
one default.
|
||||||
|
- *Costs:* existing users must opt in (they already have the 40 behavior, so no
|
||||||
|
regression).
|
||||||
|
- *Why keep default 40:* backward compatibility is a hard project value; silently
|
||||||
|
shifting the default changes session context for *every* user, discards history
|
||||||
|
they may rely on, and is a behavioral change not requested globally (only for this
|
||||||
|
user). Keep 40.
|
||||||
|
|
||||||
|
**Option 2 — change the default to 10.**
|
||||||
|
- *Advantages:* meets the stated want out-of-the-box.
|
||||||
|
- *Costs:* silent behavior change for all users; discards memory; not requested
|
||||||
|
globally. Rejected — keep the change opt-in via the new var.
|
||||||
|
|
||||||
|
### Semantics (must be documented)
|
||||||
|
The existing prune is `.messages |= .[-N:]` where N counts **messages** — 2 messages
|
||||||
|
per exchange. So `AI_SESSION_TURNS=10` ⇒ last **5** exchanges (5 user + 5 assistant).
|
||||||
|
The config description must state: "message count (2 per exchange); 10 = last 5
|
||||||
|
exchanges".
|
||||||
|
|
||||||
|
### Implementation constraint — lazy resolution (important)
|
||||||
|
`MAX_SESSION_TURNS` is currently assigned at `bin/pos-ai:25`, which executes at
|
||||||
|
top-level **before** `load_config` is first called (line 681). If we wrote
|
||||||
|
`MAX_SESSION_TURNS="${AI_SESSION_TURNS:-40}"` at line 25, an `AI_SESSION_TURNS` set
|
||||||
|
*only in ai.env* would not yet be loaded → always 40.
|
||||||
|
|
||||||
|
Therefore:
|
||||||
|
- Keep **line 25** as-is (`MAX_SESSION_TURNS=40`), used for the help text (`:67`
|
||||||
|
shows the default, accurate).
|
||||||
|
- In `session_push()` (lines 285-290), resolve **lazily**:
|
||||||
|
`local n="${AI_SESSION_TURNS:-$MAX_SESSION_TURNS}"` and use `$n` in the jq prune.
|
||||||
|
Because `session_push` runs inside `cmd_ask`/`cmd_chat` — after `load_config`
|
||||||
|
(via `resolve_key` at `:499`/`:538`) has exported `AI_SESSION_TURNS` into the
|
||||||
|
process env — the ai.env value is honored. Shell-exported `AI_SESSION_TURNS`
|
||||||
|
wins too (env-wins in `load_env_file`).
|
||||||
|
- Help text `bin/pos-ai:67` stays accurate ("capped at $MAX_SESSION_TURNS turns")
|
||||||
|
since the default remains 40. Optionally add a `Config:` help line documenting the
|
||||||
|
var — recommended, and it confirms "turns = messages, 10 = 5 pairs".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision 3: `# POS_CONFIG:` header change
|
||||||
|
|
||||||
|
**Status: [DECIDED]**
|
||||||
|
|
||||||
|
- Single `@General` section addition (not per-provider `PROVIDER_CONFIG`):
|
||||||
|
- `AI_MAX_TOKENS=num:Max output tokens per request (default 2048; OpenRouter/Gemini cost cap)`
|
||||||
|
- `AI_SESSION_TURNS=num:Session message cap — 2 per exchange (default 40 = 20 exchanges; 10 = last 5)`
|
||||||
|
- Place both in the `@General` section alongside `AI_SYSTEM_PROMPT` (end of the
|
||||||
|
long header line, `bin/pos-ai:6`).
|
||||||
|
- **Why @General, not PROVIDER_CONFIG:** the default is shared across providers
|
||||||
|
(2048, 40) and both caps are tool-level concerns. `PROVIDER_CONFIG` is reserved for
|
||||||
|
provider-specific keys (API keys, models). A single global declaration is the
|
||||||
|
cleanest and avoids duplicating the default in two provider files.
|
||||||
|
- `num:` type confirmed suitable: `lib/config-ui.sh:419` enforces integer on entry;
|
||||||
|
empty=keep current, `-`=clear (`:468-477`); unset → code default. No empty-parse
|
||||||
|
concern.
|
||||||
|
- Provider bodies consume the vars from env, so no `PROVIDER_CONFIG` additions are
|
||||||
|
needed on `openrouter.sh`/`gemini.sh`. (They could be added later if per-provider
|
||||||
|
caps are ever wanted — out of scope now.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision 4: Docs & gates
|
||||||
|
|
||||||
|
**Status: [DECIDED]**
|
||||||
|
|
||||||
|
- **`DOC/POS.md`** (hand-maintained):
|
||||||
|
- ai.env table (`:88-97`): add rows for `AI_MAX_TOKENS` (no/`2048`/"Max output
|
||||||
|
tokens per request (OpenRouter/Gemini cost cap)") and `AI_SESSION_TURNS`
|
||||||
|
(no/`40`/"Session message cap — 2 per exchange; 10 = last 5 exchanges"). The
|
||||||
|
`AI_SYSTEM_PROMPT` row (`:93`) is the placement anchor.
|
||||||
|
- Line 64 text "capped at 40 turns" remains true (default unchanged) — no edit
|
||||||
|
strictly needed, but a short parenthetical "(configurable via AI_SESSION_TURNS)"
|
||||||
|
is recommended.
|
||||||
|
- **`DOC/HOWTO.md:45`** — append `AI_MAX_TOKENS`, `AI_SESSION_TURNS` to the listed
|
||||||
|
ai.env vars.
|
||||||
|
- **`DOC/AGENT_Context_Project.md:491`** — append the two vars to the ai.env
|
||||||
|
summary parenthetical (hand-maintained).
|
||||||
|
- **`make gen`**: header text change does not add commands/subcommands/flags, so the
|
||||||
|
generated tree/dispatch tables are unaffected; `completions/pos.bash` config-scope
|
||||||
|
table regenerates to include the new keys. Run `make gen` (deterministic, `LC_ALL=C`
|
||||||
|
per convention), then `make check`, then `make lint`.
|
||||||
|
- **Line-count rows** above the filetable marker in `AGENT_Context_Project.md`: only
|
||||||
|
bump a row if a `pos-*`/`lib/*` file's length changes (it does — lib/ai-providers
|
||||||
|
grow; bin/pos-ai grows). Do not touch rows for files that don't change.
|
||||||
|
- **tests**: existing suite does not pin the session default or provider bodies, so
|
||||||
|
nothing is *forced*. Recommend new coverage (§Testing).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision 5: Scope fence
|
||||||
|
|
||||||
|
**Status: [DECIDED]**
|
||||||
|
|
||||||
|
**Approved outcome:** OpenRouter 402 eliminated (explicit capped `max_tokens` on
|
||||||
|
remote providers) and session history bounded via configurable `AI_SESSION_TURNS`.
|
||||||
|
|
||||||
|
**In-scope files:**
|
||||||
|
- `bin/pos-ai` — `# POS_CONFIG:` header (line 6, @General additions); `session_push`
|
||||||
|
lazy `AI_SESSION_TURNS` resolution (lines 285-290); optional `Config:` help lines
|
||||||
|
for the two vars. Line 25 stays `MAX_SESSION_TURNS=40`.
|
||||||
|
- `lib/ai-providers/openrouter.sh` — add `max_tokens` to body (lines 22-23).
|
||||||
|
- `lib/ai-providers/gemini.sh` — add `generationConfig.maxOutputTokens` (lines 17-19).
|
||||||
|
- `lib/ai-providers/llamacpp.sh` — **no change**.
|
||||||
|
- Docs (hand-maintained): `DOC/POS.md`, `DOC/HOWTO.md`, `DOC/AGENT_Context_Project.md`.
|
||||||
|
- tests (later, Tester).
|
||||||
|
|
||||||
|
**Allowed interface changes:** two new config keys in scope `ai`; provider request
|
||||||
|
bodies gain a token cap. Provider call signature `provider_generate "$model" "$messages" "$system"`
|
||||||
|
is unchanged.
|
||||||
|
|
||||||
|
**Explicitly out of scope:** `bin/pos-ai-server`, chat/alias wrappers, `pos-ai-alias`,
|
||||||
|
HuggingFace downloader, other tools, per-provider caps, changing `MAX_SESSION_TURNS`
|
||||||
|
default, any llmacpp body change.
|
||||||
|
|
||||||
|
**Architectural constraints:**
|
||||||
|
- No new provider-side config plumbing; vars read from env (`AI_*` convention).
|
||||||
|
- Backward compatible: session default 40 and empty/unset values fall back to
|
||||||
|
documented defaults.
|
||||||
|
- Deterministic `make gen`; `make check` + `make lint` green (definition of done).
|
||||||
|
|
||||||
|
**Open risks:**
|
||||||
|
- `AI_MAX_TOKENS` too low truncates long `--full` answers — default 2048 mitigates;
|
||||||
|
user can raise.
|
||||||
|
- env-vs-file precedence: exported env var beats config file (per `load_env_file`) —
|
||||||
|
expected and documented.
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- `bash -n` clean; `make gen`/`make check`/`make lint` green.
|
||||||
|
- A 402 reproduction no longer triggers when `AI_MAX_TOKENS` is set/at default.
|
||||||
|
- `AI_SESSION_TURNS=10` prunes the session to last 5 exchanges.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing budget (suggestion for Tester / Builder verification)
|
||||||
|
|
||||||
|
- **Provider body cap:** source each provider adapter in a sandbox with `curl`
|
||||||
|
stubbed, assert the request JSON contains `max_tokens` (openrouter) / a truthy
|
||||||
|
`generationConfig.maxOutputTokens` (gemini). Pattern: `t-config-precedence.sh` style
|
||||||
|
curl-log capture with a fake `curl` in `PATH` (see existing tests).
|
||||||
|
- **Session pruning:** drive `session_push` with a crafted messages JSON and
|
||||||
|
`AI_SESSION_TURNS=10`, assert exactly the last 10 messages remain (5 pairs); and
|
||||||
|
`AI_SESSION_TURNS` unset → 40 retained (default commits to backward compat).
|
||||||
|
- **Help/default fidelity:** `pos ai --help` still shows 40; `pos config ai` lists the
|
||||||
|
two new `num:` keys and rejects a non-integer (`cfg_validate`).
|
||||||
|
- **Regression:** existing `t-config-precedence.sh`, `run-tests` full suite green.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
**Status:** DECISION_READY
|
||||||
|
|
||||||
|
**Problem:** OpenRouter 402 (no `max_tokens` → full worst-case pre-check) and
|
||||||
|
unbounded session memory; user wants 5 req/response.
|
||||||
|
|
||||||
|
**Decision:** add `AI_MAX_TOKENS=num` (default 2048) honored by openrouter + gemini
|
||||||
|
(skip llamacpp); add `AI_SESSION_TURNS=num` (default 40, backward compatible) resolved
|
||||||
|
lazily in `session_push`; both in `@General` config section; docs updated.
|
||||||
|
|
||||||
|
**Ownership:** `bin/pos-ai` + `lib/ai-providers/{openrouter,gemini}.sh` (+ docs).
|
||||||
|
|
||||||
|
**Interfaces:** two new `ai`-scope config keys; provider bodies gain a token cap.
|
||||||
|
Call signature unchanged.
|
||||||
|
|
||||||
|
**Approved scope / constraints / verification / out-of-scope:** see Decision 5.
|
||||||
|
|
||||||
|
**Risks:** see Decision 5 (token-cap truncation; env-vs-file precedence — all mitigated).
|
||||||
|
|
||||||
|
**Recommended next agent:** **Builder**
|
||||||
|
|
||||||
|
**Reason:** the architecture and scope are fully specified with exact line-level
|
||||||
|
changes (no architectural ambiguity left). Builder can implement without making
|
||||||
|
architecture decisions. Tester follows for the recommended coverage.
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
# Builder Report — AI cost & session-window design
|
||||||
|
|
||||||
|
**Date:** 2026-09-06
|
||||||
|
**HEAD:** 8ce5479 (clean tree; HEAD unchanged — implementation not committed)
|
||||||
|
**Design source:** `./AgentsReport/architect/2026-09-06_ai-cost-window-design.md`
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
- **Status: IMPLEMENTED** (worktree changes uncommitted)
|
||||||
|
- **Files changed:** `bin/pos-ai`, `lib/ai-providers/openrouter.sh`, `lib/ai-providers/gemini.sh`, `DOC/POS.md`, `DOC/HOWTO.md`, `DOC/AGENT_Context_Project.md` (6 files, +18/−11; `make gen` auto-updated the `bin/pos-ai` filetable row 705→709)
|
||||||
|
- **Smoke:** provider-body cap (openrouter + gemini, default 2048 + custom 512) all pass; session_push lazy resolution + numeric guard all pass (incl. boundary 40, `10`, non-numeric/0/negative/empty fallback)
|
||||||
|
- **Gates:** `make gen` idempotent, `make check` OK, `make lint` 0 FAIL 0 WARN, `make test` 17/17 files + 299/299 checks, `git diff --check` clean, `bash -n` all touched scripts OK
|
||||||
|
- **Out-of-scope:** no changes to llamacpp.sh, pos-ai-server, aliases, other tools, tests, AGENT_TODO.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: POS_CONFIG header — add AI_MAX_TOKENS and AI_SESSION_TURNS
|
||||||
|
|
||||||
|
Append two entries to the `@General` section of `# POS_CONFIG:` header at `bin/pos-ai:6`.
|
||||||
|
|
||||||
|
- `AI_MAX_TOKENS=num:Max output tokens per request (default 2048; OpenRouter/Gemini cost cap)`
|
||||||
|
- `AI_SESSION_TURNS=num:Session message cap — 2 per exchange (default 40 = 20 exchanges; 10 = last 5)`
|
||||||
|
|
||||||
|
Wording matches the design report §Decision 3 exactly. `num:` type reuses the existing `LLAMACPP_CTX_SIZE=num:…` pattern in the same header.
|
||||||
|
|
||||||
|
[DONE]
|
||||||
|
|
||||||
|
## Step 2: session_push lazy resolution
|
||||||
|
|
||||||
|
Change `session_push()` (`bin/pos-ai:285-292`) to resolve `AI_SESSION_TURNS` lazily with numeric guard.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
local n="${AI_SESSION_TURNS:-$MAX_SESSION_TURNS}"
|
||||||
|
[[ "$n" =~ ^[0-9]+$ ]] && (( n >= 1 )) || n="$MAX_SESSION_TURNS"
|
||||||
|
```
|
||||||
|
|
||||||
|
- `^[0-9]+$` regex + `>= 1` → fallback on: empty, non-numeric, `0`, negative.
|
||||||
|
- Line 25 `MAX_SESSION_TURNS=40` untouched (help text `:67` still accurate).
|
||||||
|
- jq pruning now uses `--argjson n "$n" ... .[-$n:]` (no string interpolation into the filter).
|
||||||
|
|
||||||
|
[DONE]
|
||||||
|
|
||||||
|
## Step 3: Add Config help lines for the two new vars
|
||||||
|
|
||||||
|
Added to the `Config:` block in `pos ai --help` (`bin/pos-ai:95-96`):
|
||||||
|
`AI_MAX_TOKENS` + `AI_SESSION_TURNS` lines, matching the design's recommended `Config:` help documentation.
|
||||||
|
|
||||||
|
[DONE]
|
||||||
|
|
||||||
|
## Step 4: openrouter.sh — add max_tokens to body
|
||||||
|
|
||||||
|
`lib/ai-providers/openrouter.sh:22-23`: body now includes `max_tokens:($mt|tonumber)` via `--arg mt "${AI_MAX_TOKENS:-2048}"`. Empty/unset env → jq receives the literal `2048` (the `:-2048` default is a numeric literal, so there is no empty-string `tonumber` failure path).
|
||||||
|
|
||||||
|
[DONE]
|
||||||
|
|
||||||
|
## Step 5: gemini.sh — add generationConfig.maxOutputTokens
|
||||||
|
|
||||||
|
`lib/ai-providers/gemini.sh:17-19`: body now includes `generationConfig: {maxOutputTokens: ($mt|tonumber)}` via the same `--arg mt "${AI_MAX_TOKENS:-2048}"`. `systemInstruction` merging (`:20-23`) unchanged and composes with the new top-level field.
|
||||||
|
|
||||||
|
[DONE]
|
||||||
|
|
||||||
|
## Step 6: Doc updates
|
||||||
|
|
||||||
|
- `DOC/POS.md:93-95` ai.env table: two new rows after `AI_SYSTEM_PROMPT` (`AI_MAX_TOKENS` no/`2048`/"Max output tokens per request (OpenRouter/Gemini cost cap)"; `AI_SESSION_TURNS` no/`40`/"Session message cap — 2 per exchange; 10 = last 5 exchanges").
|
||||||
|
- `DOC/POS.md:64`: parenthetical "(configurable via `AI_SESSION_TURNS`)" added after "capped at 40 turns" — recommended edit from design §Decision 4.
|
||||||
|
- `DOC/HOWTO.md:45`: `AI_MAX_TOKENS`, `AI_SESSION_TURNS` inserted after `AI_SYSTEM_PROMPT` in the `ai.env` var list.
|
||||||
|
- `DOC/AGENT_Context_Project.md:491`: same two vars added to the ai.env summary parenthetical.
|
||||||
|
- Line-count rows (hand-maintained, `:595-613`): no bump needed — none of those rows' files (install.sh, lib/*, features/*) changed length. `bin/pos-ai` row lives in the GENERATED filetable and was auto-updated 705 → 709 by `make gen`.
|
||||||
|
|
||||||
|
[DONE]
|
||||||
|
|
||||||
|
## Step 7: make gen x2 + make check + make lint
|
||||||
|
|
||||||
|
- `make gen` ran 3× total (twice at implementation, once after the test suite): each rerun is a no-op → **idempotent**.
|
||||||
|
- Generated delta is ONLY the `bin/pos-ai` line-count row (705 → 709) in the filetable block. `completions/pos.bash` unchanged — verified `_pos_config_scopes` (`completions/pos.bash:55`) lists only scope *names* (already includes `ai`); individual keys are read live by `pos config` via `lib/config-ui.sh` from the `# POS_CONFIG:` headers (confirmed via `lib/registry.sh` `reg_scan bin` + `reg_config_keys ai` showing both new keys). The design's expectation that completions would gain the keys is a no-op in practice — no key-level completion table exists by design.
|
||||||
|
- `make check` (check-sync.sh): **OK**
|
||||||
|
- `make lint` (lint-conventions.sh): **0 FAIL, 0 WARN**
|
||||||
|
|
||||||
|
[DONE]
|
||||||
|
|
||||||
|
## Step 8: Smoke tests (no network — fake curl shim in /tmp/opencode/ai-cost-smoke)
|
||||||
|
|
||||||
|
### Provider body cap (`test-provider-body.sh`, 12 checks, exit 0)
|
||||||
|
|
||||||
|
Fake `curl` on PATH captures the `--data` body to a file and returns a canned 200 response + the `\n%{http_code}` line the adapters expect.
|
||||||
|
|
||||||
|
| Case | Body assertion | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| openrouter, AI_MAX_TOKENS unset | `"max_tokens":2048` | OK |
|
||||||
|
| openrouter, unset — JSON shape | model kept, messages kept | OK |
|
||||||
|
| openrouter, unset — canned response | `hello from fake openrouter` round-trips | OK |
|
||||||
|
| openrouter, AI_MAX_TOKENS=2048 | `"max_tokens":2048` | OK |
|
||||||
|
| openrouter, AI_MAX_TOKENS=512 | `"max_tokens":512` | OK |
|
||||||
|
| gemini, AI_MAX_TOKENS unset | `generationConfig.maxOutputTokens == 2048` | OK |
|
||||||
|
| gemini, unset — role conversion | assistant→model (unchanged behavior) | OK |
|
||||||
|
| gemini, unset — no systemInstruction when empty | absent | OK |
|
||||||
|
| gemini, unset — canned response | `hello from fake gemini` parses + round-trips | OK |
|
||||||
|
| gemini, AI_MAX_TOKENS=512 | `generationConfig.maxOutputTokens == 512` | OK |
|
||||||
|
|
||||||
|
Canned bodies (shim): openrouter `{"choices":[{"message":{"role":"assistant","content":"hello from fake openrouter"}}]}`, gemini `{"candidates":[{"content":{"parts":[{"text":"hello from fake gemini"}]}}]}`; both + `200`.
|
||||||
|
|
||||||
|
### session_push lazy resolution (`test-session-push.sh`, 10 checks, exit 0)
|
||||||
|
|
||||||
|
`session_push` brace-extracted from `bin/pos-ai` (same `extract_fn` pattern as `tests/t-menu-allow-empty.sh`; function's braces are balanced so extraction is exact), with `MAX_SESSION_TURNS=40` declared as in `bin/pos-ai:25`.
|
||||||
|
|
||||||
|
| Input | AI_SESSION_TURNS | Output length | Result |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 30 msgs + 1 push | unset | 31 (no truncation under default) | OK |
|
||||||
|
| 30 msgs + 1 push | `10` | 10 (5 exchanges) | OK |
|
||||||
|
| 30 msgs + 1 push | `abc` (non-numeric) | 31 (fallback 40 → no truncation) | OK |
|
||||||
|
| 39 msgs + 1 push | unset | **40** (exact default boundary) | OK |
|
||||||
|
| 39 msgs + 1 push | `10` | 10 | OK |
|
||||||
|
| 39 msgs + 1 push | `0` (<1) | 40 (fallback) | OK |
|
||||||
|
| 39 msgs + 1 push | `-5` (negative) | 40 (fallback) | OK |
|
||||||
|
| 39 msgs + 1 push | `''` (empty) | 40 (fallback) | OK |
|
||||||
|
|
||||||
|
Note: the brief's "30-message array → 40 default" cannot be exact (30+1=31 < 40, so nothing is trimmed); the 39+1 boundary case proves the exact default of 40.
|
||||||
|
|
||||||
|
### Config registry discovery
|
||||||
|
|
||||||
|
`reg_scan bin` + `reg_config_keys ai` via `lib/registry.sh` returns both new keys in the `ai` scope (`AI_MAX_TOKENS=num:…`, `AI_SESSION_TURNS=num:…`) — `pos config ai` will present them.
|
||||||
|
|
||||||
|
### Full test suite
|
||||||
|
|
||||||
|
`make test`: **17 files pass / 0 fail / 0 skip; 299 checks pass / 0 fail / 0 skip** (matches the 17/299 budget).
|
||||||
|
|
||||||
|
### Other gates
|
||||||
|
|
||||||
|
- `bash -n bin/pos-ai lib/ai-providers/openrouter.sh lib/ai-providers/gemini.sh` — clean
|
||||||
|
- `git diff --check` — clean
|
||||||
|
- `make gen` idempotent; `make check` OK; `make lint` 0 FAIL 0 WARN
|
||||||
|
|
||||||
|
[DONE]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files changed (diff summary)
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|---|---|
|
||||||
|
| `bin/pos-ai` | POS_CONFIG header +2 keys; help Config block +2 lines; session_push lazy + guard (+4 net) |
|
||||||
|
| `lib/ai-providers/openrouter.sh` | body gains `max_tokens` |
|
||||||
|
| `lib/ai-providers/gemini.sh` | body gains `generationConfig.maxOutputTokens` |
|
||||||
|
| `DOC/POS.md` | table +2 rows; line 64 parenthetical |
|
||||||
|
| `DOC/HOWTO.md` | ai.env var list +2 |
|
||||||
|
| `DOC/AGENT_Context_Project.md` | ai.env parenthetical +2; generated filetable row auto-updated |
|
||||||
|
|
||||||
|
## Scope compliance
|
||||||
|
|
||||||
|
- In-scope changes only (design §Decision 5 file list). No llamacpp.sh, no pos-ai-server, no alias wrappers, no other tools, no tests, no AGENT_TODO.md edits.
|
||||||
|
- Provider call signature unchanged (`provider_generate "$model" "$messages" "$system"`).
|
||||||
|
|
||||||
|
## Remaining risks / follow-up
|
||||||
|
|
||||||
|
- `AI_MAX_TOKENS` too low truncates long `--full` answers (default 2048 mitigates; user can raise) — design's known risk.
|
||||||
|
- Env-vs-file precedence: exported env var beats config file (`load_env_file`) — expected and documented.
|
||||||
|
- Tester is recommended to add permanent provider-body + session-pruning regression tests (design §Testing).
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
Status: IMPLEMENTED
|
||||||
|
Recommended next agent: **Reviewer** (independent adversarial review of the diff before acceptance; Tester follows for permanent coverage per design §Testing).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reviewer finding F1 fix (2026-09-06, after reviewer CHANGES_REQUIRED)
|
||||||
|
|
||||||
|
**Source:** `AgentsReport/reviewer/2026-09-06_ai-cost-window-review.md` Finding 1 (REQUIRED) + Finding 2 (NOTE, folded).
|
||||||
|
|
||||||
|
**Problem:** `AI_MAX_TOKENS=0`/`-5`/`010` are accepted by `pos config ai` (`num:` regex `^-?[0-9]+$`, config-ui.sh:419) and passed unguarded into jq `($mt|tonumber)` → provider 400 (0/-5) or raw jq abort (010 = invalid JSON literal; abc = tonumber error). The sibling `AI_SESSION_TURNS` guard `^[0-9]+$` also let `010` through (octal 8 ≥ 1) then aborted jq via `--argjson n "010"`.
|
||||||
|
|
||||||
|
**Fix (exactly per reviewer demand, ~4 lines):**
|
||||||
|
|
||||||
|
1. `lib/ai-providers/openrouter.sh:22-23`:
|
||||||
|
```bash
|
||||||
|
local mt="${AI_MAX_TOKENS:-2048}"
|
||||||
|
[[ "$mt" =~ ^[1-9][0-9]*$ ]] || mt=2048
|
||||||
|
```
|
||||||
|
then `--arg mt "$mt"` (previously `--arg mt "${AI_MAX_TOKENS:-2048}"`), keep `($mt|tonumber)`.
|
||||||
|
2. `lib/ai-providers/gemini.sh:17-18`: identical guard + use.
|
||||||
|
3. `bin/pos-ai:290-291` session guard tightened `^[0-9]+$` → `^[1-9][0-9]*$`; the `(( n >= 1 ))` check is now redundant (regex guarantees ≥ 1) and was dropped cleanly:
|
||||||
|
```bash
|
||||||
|
local n="${AI_SESSION_TURNS:-$MAX_SESSION_TURNS}"
|
||||||
|
[[ "$n" =~ ^[1-9][0-9]*$ ]] || n="$MAX_SESSION_TURNS"
|
||||||
|
```
|
||||||
|
`--argjson n "$n"` is safe once n is strictly decimal (`[1-9][0-9]*` is a valid JSON numeric literal — no leading zeros).
|
||||||
|
|
||||||
|
**Re-verification (same harness, extended cases):**
|
||||||
|
|
||||||
|
| Check | Result |
|
||||||
|
|---|---|
|
||||||
|
| bash -n all three files | OK |
|
||||||
|
| Provider smoke (extended): AI_MAX_TOKENS unset / `0` / `-5` / `010` / `abc` → body `"max_tokens":2048`; `512` → `"max_tokens":512`; openrouter + gemini both | 16 checks OK |
|
||||||
|
| session_push smoke (extended): AI_SESSION_TURNS `0` / `010` / `-5` / `abc` → output length == default (31 for 30-array / 40 for 39-array boundary); `10` → 10 | 12 checks OK |
|
||||||
|
| `make gen` idempotent (POS_CONFIG unchanged; no new diff) | OK |
|
||||||
|
| `make check` | OK |
|
||||||
|
| `make lint` | 0 FAIL, 0 WARN |
|
||||||
|
| `make test` | 17 files / 299 checks green |
|
||||||
|
| `git diff --check` | clean |
|
||||||
|
|
||||||
|
**Smoke matrix detail (provider body, openrouter + gemini):**
|
||||||
|
|
||||||
|
| AI_MAX_TOKENS | openrouter body | gemini body |
|
||||||
|
|---|---|---|
|
||||||
|
| unset | `max_tokens:2048` | `maxOutputTokens:2048` |
|
||||||
|
| (empty) | `max_tokens:2048` | `maxOutputTokens:2048` |
|
||||||
|
| `0` | `max_tokens:2048` (fallback) | `maxOutputTokens:2048` (fallback) |
|
||||||
|
| `-5` | `max_tokens:2048` (fallback) | `maxOutputTokens:2048` (fallback) |
|
||||||
|
| `010` | `max_tokens:2048` (fallback, no jq abort) | `maxOutputTokens:2048` (fallback, no jq abort) |
|
||||||
|
| `abc` | `max_tokens:2048` (fallback, no jq abort) | `maxOutputTokens:2048` (fallback, no jq abort) |
|
||||||
|
| `512` | `max_tokens:512` | `maxOutputTokens:512` |
|
||||||
|
|
||||||
|
**session_push matrix (30-msg array + 1 push):**
|
||||||
|
|
||||||
|
| AI_SESSION_TURNS | output length | result |
|
||||||
|
|---|---|---|
|
||||||
|
| unset | 31 (all, default 40 no-op) | OK |
|
||||||
|
| `10` | 10 | OK |
|
||||||
|
| `0` | 31 (fallback 40) | OK |
|
||||||
|
| `010` | 31 (fallback 40 — former jq abort hole closed) | OK |
|
||||||
|
| `-5` | 31 (fallback 40) | OK |
|
||||||
|
| `abc` | 31 (fallback 40) | OK |
|
||||||
|
|
||||||
|
**Scope compliance:** only the 3 already-approved files touched in this fix round; no design change, no jq logic shape change (still `($mt|tonumber)` / `generationConfig: {maxOutputTokens: …}`), no new files (report appended).
|
||||||
|
|
||||||
|
[Handoff priority for F1 fix: Reviewer re-verify → Orchestrator gates → Tester permanent coverage; same recommendation as original handoff.]
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Maintainer Report — AGENT_TODO.md Done entry for AI cost/session-window work
|
||||||
|
|
||||||
|
**Date:** 2026-09-06
|
||||||
|
**Mode:** single-file convention edit (AGENT_TODO.md only); no commit; no gates.
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
Drift: the AI cost & session-window work (OpenRouter 402 + AI_SESSION_TURNS) was finished but had no **Done** entry — a one-edit-per-commit convention violation. Fix: added ONE dated **Done** bullet at the TOP of the Done section (2026-09-06, above the existing same-day entries), ~5-8 lines, style-matched to existing entries. Corrected, validated against diff (2 insertions), report written. All constraints honored: touched ONLY `AGENT_TODO.md`, ran no gates, made no commit.
|
||||||
|
|
||||||
|
## Step 1: Verify facts before writing
|
||||||
|
|
||||||
|
Read `AgentsReport/architect/2026-09-06_ai-cost-window-design.md` (decisions, defaults, lazy resolution, scope) and `AgentsReport/builder/2026-09-06_ai-cost-window.md` (implementation + F1 hardening + verification numbers). Cross-checked reviewer `2026-09-06_f1-hardening-recheck.md` → Status **ACCEPT** (after `2026-09-06_ai-cost-window-review.md` → **CHANGES_REQUIRED**, 1 REQUIRED + 1 NOTE) → supports "Reviewer ACCEPT (twice)" with the CHANGES_REQUIRED→fixed flow.
|
||||||
|
|
||||||
|
Confirmed the Done-section convention from existing entries (line 6 "newest last", bullet `- **YYYY-MM-DD** — <title> …` wrapping one paragraph).
|
||||||
|
|
||||||
|
[DONE]
|
||||||
|
|
||||||
|
## Step 2: Insert the Done entry
|
||||||
|
|
||||||
|
Added one bullet at the top of the `## Done` section (`AGENT_TODO.md:45`), placing it **above** the existing `pos ai alias` same-day entry (per brief: "at the TOP of the Done section … above the existing same-day entries"). Covers:
|
||||||
|
|
||||||
|
- 402 root cause (no `max_tokens` sent; OpenRouter pre-bills worst-case 131072 vs 4511 balance) + user request to bound session to last-5.
|
||||||
|
- Architect decisions: `AI_MAX_TOKENS` (num, default 2048, cost cap) on OpenRouter `max_tokens` + Gemini `generationConfig.maxOutputTokens` (llamacpp skipped); `AI_SESSION_TURNS` (2 msgs/exchange; default 40 back-compat; 10 = last 5) resolved lazily in `session_push` (config loads after line-25 default); both registered in `bin/pos-ai` `# POS_CONFIG:` `@General` → `pos config ai`.
|
||||||
|
- Reviewer hardening (CHANGES_REQUIRED→fixed): unguarded env → jq `tonumber` (0/-5/010/abc) — both providers + `session_push` now guard `^[1-9][0-9]*$` fallback-to-default.
|
||||||
|
- Verified: fake-curl shim (16 provider-body + 12 session-window checks incl. 010-regression proof), gen idempotent, check OK, lint 0/0, test 17 files/299 green, Reviewer ACCEPT (twice). Tester round not run (user's call); permanent coverage = follow-up.
|
||||||
|
|
||||||
|
Line count: the bullet is ~6 wrapped display lines (single logical line / 2 added physical lines).
|
||||||
|
|
||||||
|
[DONE]
|
||||||
|
|
||||||
|
## Step 3: Validate the correction
|
||||||
|
|
||||||
|
`git diff --stat AGENT_TODO.md` → `1 file changed, 2 insertions(+)`; `git diff AGENT_TODO.md` shows only the one added logical line at the top of Done, no other files touched. Verified the entry is above the existing same-day `pos ai alias` entry and style-matches (bullet, `- **2026-09-06** —`, inline prose).
|
||||||
|
|
||||||
|
Per constraints: no gates run, no commit made.
|
||||||
|
|
||||||
|
[DONE]
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
Status: MAINTENANCE_COMPLETE (single-item task, fully satisfied)
|
||||||
|
|
||||||
|
Standard enforced: AGENT_TODO.md **Done** convention — move completed work into **Done** (dated) in the same-language form used by existing entries.
|
||||||
|
|
||||||
|
Files changed: `AGENT_TODO.md` (1 logical line added at top of Done).
|
||||||
|
|
||||||
|
Verification: `git diff --stat` / `git diff` — 2 insertions only; correct placement + style confirmed by read.
|
||||||
|
|
||||||
|
Scope compliance: in-scope (AGENT_TODO.md only) — no other files, no gates, no commit.
|
||||||
|
|
||||||
|
Recommended next agent: Orchestrator (this edit rides the imminent commit; no further Maintainer action).
|
||||||
|
|
||||||
|
Reason: nothing else remains in scope; the entry is ready to be committed with the AI cost/session-window work.
|
||||||
|
|
||||||
|
Changes made by Maintainer: one **Done** entry added at the top of the Done section.
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
# Reviewer Report — AI cost & session-window change review
|
||||||
|
|
||||||
|
**Date:** 2026-09-06
|
||||||
|
**Reviewer:** big-pickle (read-only, adversarial)
|
||||||
|
**HEAD reviewed:** 8ce5479 (working tree has uncommitted Builder changes)
|
||||||
|
**Design contract:** `AgentsReport/architect/2026-09-06_ai-cost-window-design.md`
|
||||||
|
**Builder handoff:** `AgentsReport/builder/2026-09-06_ai-cost-window.md`
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
- **Status: CHANGES_REQUIRED** — 1 REQUIRED finding (token-cap input guard), 1 NOTE.
|
||||||
|
- **Scope:** diff is exactly the 6 approved files (+2 journal reports); no llamacpp.sh / pos-ai-server / aliases / tests / AGENT_TODO edits. Verified.
|
||||||
|
- **Core fix verified statically:** openrouter + gemini now send a hard `max_tokens` cap (default 2048 on unset/empty); `session_push` resolves `AI_SESSION_TURNS` lazily with a numeric guard; help text and all three docs are truthful and consistent; registry/header format is key-driven so both new keys are discoverable in scope `ai`.
|
||||||
|
- **Defect:** `AI_MAX_TOKENS=0` / `-5` / `010` are *accepted by `pos config ai`* (`num:` validation regex is `^-?[0-9]+$`) and reach jq with no guard → provider 400s (0/-5) or a raw jq abort (leading zeros, non-numeric via hand-edit). The sibling `AI_SESSION_TURNS` var is guarded; `AI_MAX_TOKENS` is not, and the session guard itself still misses leading-zero values.
|
||||||
|
- **UNVERIFIED (sandbox denies execution):** `make gen` idempotence, `make check`, `make lint`, `make test` (Builder claims gen x3 no-op, check OK, lint 0 FAIL/0 WARN, 17 files/299 checks green), live `reg_config_keys ai` / `pos config ai` rendering, and the 402 live repro. Static evidence is consistent with all of these; the Orchestrator should run the gates before merge.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Diff scope — [PASS]
|
||||||
|
|
||||||
|
`git diff HEAD --stat` shows exactly:
|
||||||
|
|
||||||
|
- `bin/pos-ai` (+10/−6 net +4 — header line 6, help lines 94-95, `session_push` 288-293)
|
||||||
|
- `lib/ai-providers/openrouter.sh` (+2/−2, body 22-23)
|
||||||
|
- `lib/ai-providers/gemini.sh` (+3/−2, body 17-20)
|
||||||
|
- `DOC/POS.md`, `DOC/HOWTO.md`, `DOC/AGENT_Context_Project.md`
|
||||||
|
|
||||||
|
`git status --porcelain` = 6 modified files + 2 untracked journal reports only. **No** llamacpp.sh (`lib/ai-providers/llamacpp.sh:32-33` verified unchanged), no pos-ai-server, no alias wrappers, no tests, no AGENT_TODO, no `completions/pos.bash`. Matches design §Decision 5 file list exactly. `git diff --check` clean.
|
||||||
|
|
||||||
|
## Step 2: max_tokens correctness — [PASS] with edge finding
|
||||||
|
|
||||||
|
- Default applied: `--arg mt "${AI_MAX_TOKENS:-2048}"` → unset **or empty-exported** env → jq gets `"2048"` → `tonumber` → 2048. Applies in both providers. Var name matches the `# POS_CONFIG:` declaration (`AI_MAX_TOKENS`, bin/pos-ai:6); `cfg_write` writes the header key verbatim, so `pos config ai` writes `AI_MAX_TOKENS`/`AI_SESSION_TURNS` into ai.env; `load_env_file` (config-ui.sh:336-357) exports with env-wins. Providers read the same names.
|
||||||
|
- Ordering safe: `session_push`/`provider_generate` are only reached after `require_key` → `resolve_key` → `load_config` (cmd_ask:503→524-529, cmd_chat:542→560-565), so ai.env values are loaded before lazy reads.
|
||||||
|
- **Findings 1 & 2** below cover `AI_MAX_TOKENS=0`/`-5`/`010`/`abc` (no guard; jq `tonumber` semantics) and `AI_SESSION_TURNS=010` slipping through the guard.
|
||||||
|
|
||||||
|
## Step 3: session window correctness — [PASS] (with NOTE)
|
||||||
|
|
||||||
|
- `session_push` (pos-ai:288-293): `local n="${AI_SESSION_TURNS:-$MAX_SESSION_TURNS}"`; guard `^[0-9]+$` + `(( n >= 1 ))` else 40; jq `--argjson n` + `.messages |= .[-$n:]` (no string interpolation into the filter — improvement over the old `'"$MAX_SESSION_TURNS"'` splice).
|
||||||
|
- n=10 → append user+assistant then keep exactly last 10 messages = 5 exchanges. ✓ matches user goal.
|
||||||
|
- Very large arrays / n > length → jq slice clamps to whole array, no error. ✓
|
||||||
|
- `SESSION` is always `default`/`--session` value (pos-ai:23, 658-660); `session_push` does not depend on SESSION at all and `session_load`/`session_save` early-return on empty SESSION (252-253, 277). No unsafe path.
|
||||||
|
- Help text: pos-ai:67 renders "capped at 40 turns" (default 40 — truthful); the new Config: lines (94-95) document both vars with the "10 = last 5" semantics. DOC/POS.md:64 adds "(configurable via `AI_SESSION_TURNS`)". Truthful.
|
||||||
|
- NOTE: `AI_SESSION_TURNS=010` passes the guard (regex matches; bash octal eval 8 ≥ 1) but `--argjson n "010"` is an invalid JSON numeric literal → jq abort under `set -e`. Pathological but config-ui-enterable. Covered by the same fix as Findings 1/2.
|
||||||
|
|
||||||
|
## Step 4: Config surface — [PASS: static] / [UNVERIFIED: runtime]
|
||||||
|
|
||||||
|
- Registry (`lib/registry.sh`) is format-driven: `reg_config_keys <scope>` echoes the whole `# POS_CONFIG:` line(s) per scope; the `ai` scope line (pos-ai:6) now contains `AI_MAX_TOKENS=num:…` and `AI_SESSION_TURNS=num:…` inside `@General`, exactly matching the proven `LLAMACPP_CTX_SIZE=num:…` pattern. config-ui renders `@`-captioned sections.
|
||||||
|
- Static conclusion: both keys ARE discoverable under scope `ai` in `@General`. Runtime `reg_config_keys ai` / `pos config ai` display could not be executed (sandbox denies `source`/bash) → **UNVERIFIED**; Orchestrator/next agent can re-run.
|
||||||
|
- Note: `bin/pos-ai-hf` also declares scope `ai` (pre-existing); both lines merge under `ai` — unchanged behavior.
|
||||||
|
|
||||||
|
## Step 5: Doc sync — [PASS]
|
||||||
|
|
||||||
|
- `DOC/POS.md:94-95` new rows: `AI_MAX_TOKENS` no/`2048`/"Max output tokens per request (OpenRouter/Gemini cost cap)"; `AI_SESSION_TURNS` no/`40`/"Session message cap — 2 per exchange; 10 = last 5 exchanges" — consistent with header + code (2048 default; 40 messages = 20 exchanges; 10 = 5 exchanges). `:64` parenthetical true (default unchanged).
|
||||||
|
- `DOC/HOWTO.md:45` and `DOC/AGENT_Context_Project.md:491` append both vars. ✓
|
||||||
|
- GEN blocks: only the filetable row `bin/pos-ai 705→709` changed (inside `GEN:START filetable`/`GEN:END`, AGENT_Context 614-662); `wc -l bin/pos-ai` = 709 → the generated row is byte-consistent with the file, so a `make gen` rerun would be a no-op for that row. Other GEN blocks and `completions/pos.bash` untouched (no key-level completion table exists — config is read live from headers). Hand-maintained Key-File rows (AGENT_Context:593-613) do not include `lib/ai-providers/*`, so "no bump needed" is correct.
|
||||||
|
|
||||||
|
## Step 6: Gates — [UNVERIFIED]
|
||||||
|
|
||||||
|
Sandbox permission rules deny `make`, `bash`, `source`, `jq`, `mkdir` (allowlist: git read commands, head/tail/wc/sort/grep/rg only). Builder claims: `make gen` idempotent x3, `make check` OK, `make lint` 0 FAIL 0 WARN, `make test` 17 files/299 checks green, `bash -n` clean, `git diff --check` clean (the last confirmed independently). **Orchestrator must run `make gen && git diff --exit-code`, `make check`, `make lint`, `make test` before merge** to confirm; static review found no gen-drift predicate (no new commands/flags; header text doesn't feed the generated tables).
|
||||||
|
|
||||||
|
## Step 7: Behavioral sanity (user's 402 goal) — [PASS with caveats]
|
||||||
|
|
||||||
|
- Pre-fix: no `max_tokens` → OpenRouter pre-check bills worst-case 131072 output tokens → 402 at balance-4511. Post-fix the request body carries `max_tokens: 2048`, so the pre-check estimate ≈ input_tokens + 2048 (output) instead of + 131072. With the terse `ask` prompt (~200 input tokens) the estimate is far under the ~4511-token affordance → 402 resolved for typical requests. Math is plausible.
|
||||||
|
- Caveats for the user (also in Findings → caveats below): (1) if the auto-routed model's *output* price per token is steep, 2048 output tokens can still exceed the balance → 402 persists; set an explicit cheaper `OPENROUTER_MODEL` or switch `AI_PROVIDER=gemini`/`llamacpp`. (2) `max_tokens:2048` truncates long `--full` answers (raise the var). (3) Live OpenRouter/Gemini repro was not run (no network, no key) → UNVERIFIED.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
**Finding 1 — REQUIRED**
|
||||||
|
- **Evidence:** `lib/ai-providers/openrouter.sh:22-23` and `lib/ai-providers/gemini.sh:17-19` pass `${AI_MAX_TOKENS:-2048}` straight into jq `($mt|tonumber)` with no range/format guard. `lib/config-ui.sh:419` `num:` validation accepts `^-?[0-9]+$` — so `AI_MAX_TOKENS=0`, `-5`, `010` are all savable via `pos config ai`. Consequences: `0`/`-5` → provider 400 on every request (OpenRouter `max_tokens` must be ≥ 1; Gemini `maxOutputTokens` ≥ 1); `010` → jq invalid numeric literal → command substitution fails → `set -euo pipefail` (pos-ai:2) aborts the whole CLI with a raw jq error; same raw abort for a non-numeric value hand-set/exported (e.g. `AI_MAX_TOKENS=abc`). The sibling `AI_SESSION_TURNS` got an explicit guard (pos-ai:290-291) — the asymmetry shows the guard pattern was intended but not applied to the token cap, and the session guard itself misses leading-zero values (see Finding 2).
|
||||||
|
- **Relevant files/lines:** openrouter.sh:22-23; gemini.sh:17-19; config-ui.sh:419; bin/pos-ai:290-291.
|
||||||
|
- **Approved scope reference:** design §Decision 1 (providers read `${AI_MAX_TOKENS:-2048}`), §Decision 5 ("empty/unset values fall back to documented defaults" — satisfied, but 0/negative/leading-zero are neither empty nor unset).
|
||||||
|
- **Why it matters:** a value the project's own config UI accepts (0 — a plausible "no cap" attempt, valid on some OpenAI-compatible backends) hard-breaks both remote providers until manually cleared; the new var is the 402 fix, so its input path should fail closed to the safe default, matching the guarded sibling var.
|
||||||
|
- **Fix demand (Builder, in scope):** in both providers, before jq: `mt="${AI_MAX_TOKENS:-2048}"; [[ "$mt" =~ ^[1-9][0-9]*$ ]] || mt=2048` (also rejects 0, negatives, leading zeros, empty, non-numeric) then `--arg mt "$mt"`. 2-3 lines each.
|
||||||
|
|
||||||
|
**Finding 2 — NOTE (folded into Finding 1's fix)**
|
||||||
|
- **Evidence:** `bin/pos-ai:290-291` guard is `^[0-9]+$` + `(( n >= 1 ))`; `AI_SESSION_TURNS=010` passes both (octal 8) then `--argjson n "010"` is an invalid JSON numeric literal → jq abort. Pathological, but config-ui-enterable. Fix: switch the session guard to the same `^[1-9][0-9]*$` pattern.
|
||||||
|
|
||||||
|
**Finding 3 — NOTE (unverified runtime claims)**
|
||||||
|
- Gates/suite/live-402 claims from the Builder report could not be re-run under the sandbox (deny rules). The Orchestrator must run `make gen`/`make check`/`make lint`/`make test` and, if desired, a live OpenRouter repro before merge. Static evidence is consistent with the claims (row 709 == `wc -l`; header change adds no commands → gen tables unaffected).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification verified (by evidence)
|
||||||
|
|
||||||
|
- Diff scope exact (6 approved files; `git status`/`git diff --stat`/`git diff`).
|
||||||
|
- `git diff --check` clean.
|
||||||
|
- Default 2048 applied when `AI_MAX_TOKENS` unset or empty (`:-2048` → jq 2048).
|
||||||
|
- Var-name match across POS_CONFIG header, providers, and config-ui writer.
|
||||||
|
- Lazy `AI_SESSION_TURNS` resolution after `load_config` in all 4 `session_push` call sites; `n>=1` guard; `--argjson` numeric slicing; n=10 → exactly 10 messages = 5 exchanges; large arrays safe; help text truthful.
|
||||||
|
- Docs consistent (POS.md 94-95 + :64, HOWTO:45, AGENT_Context:491, filetable 709).
|
||||||
|
- No GEN block hand-edits (only the generated filetable row; value matches `wc -l`).
|
||||||
|
- llamacpp.sh / pos-ai-server / aliases / tests / AGENT_TODO untouched.
|
||||||
|
|
||||||
|
## Verification unverified
|
||||||
|
|
||||||
|
- `make gen` idempotence (and `git diff --exit-code` after gen), `make check`, `make lint` (0 FAIL/0 WARN), `make test` (17/299), `bash -n` — sandbox denies execution; need Orchestrator run.
|
||||||
|
- Live `reg_config_keys ai` / `pos config ai` display (static evidence strong; runtime render unverified).
|
||||||
|
- Live OpenRouter/Gemini request + 402 reproduction (no network/key; fake-curl smoke was temp, not committed; design §Testing defers permanent coverage to Tester).
|
||||||
|
|
||||||
|
## Scope compliance
|
||||||
|
|
||||||
|
- In-scope confirmed: header @General additions; session_push lazy resolution + guard; Config: help lines; openrouter `max_tokens`; gemini `generationConfig.maxOutputTokens`; three doc files; generated filetable row.
|
||||||
|
- Out-of-scope found: none.
|
||||||
|
|
||||||
|
## Remaining uncertainty
|
||||||
|
|
||||||
|
- Gate results (Builder's claims unconfirmed in-sandbox).
|
||||||
|
- Whether the auto-routed OpenRouter model's output price per token is low enough for 2048 output tokens within the ~4511-token balance (live repro needed).
|
||||||
|
- Leading-zero / 0 / negative `AI_MAX_TOKENS` behavior is *certain* from jq/bash semantics (jq `tonumber` on `"abc"` errors; `"010"` is an invalid numeric literal; `0`/`-5` are valid numbers the providers will reject).
|
||||||
|
|
||||||
|
## Verdict
|
||||||
|
|
||||||
|
**CHANGES_REQUIRED** — one REQUIRED finding (token-cap input guard), fixable in ~4 lines within the approved scope (Builder), after which the same reviewer step re-verifies; then the Orchestrator runs the four gates and Tester adds permanent provider-body + session-pruning coverage per design §Testing.
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
**Status:** CHANGES_REQUIRED
|
||||||
|
**Objective/problem:** one REQUIRED robustness finding on the new `AI_MAX_TOKENS` input path (and the same leading-zero hole in the session guard).
|
||||||
|
**Evidence:** config-ui:419 accepts 0/-5/010; providers pass them unguarded to jq; provider 400 or raw jq abort under `set -e`; sibling guard at pos-ai:290-291 proves the intended pattern.
|
||||||
|
**Affected areas:** `lib/ai-providers/openrouter.sh:22-23`, `lib/ai-providers/gemini.sh:17-19`, optionally `bin/pos-ai:290-291` guard regex.
|
||||||
|
**Scope/decision boundary:** Builder may only add the numeric guard (default-fallback on invalid); no design change.
|
||||||
|
**Verification performed:** full static diff/code/doc/registry audit; `git diff --check`; `wc -l` consistency; gates UNVERIFIED (sandbox).
|
||||||
|
**Remaining uncertainty:** gate results; live 402 repro.
|
||||||
|
**Recommended next agent:** **Builder**
|
||||||
|
**Reason:** the fix is a scoped, fully-specified 4-line hardening within the approved change; after it lands, Orchestrator runs gates and Tester adds the permanent regression tests (provider-body cap, session pruning) the design already budgets.
|
||||||
|
|
||||||
|
**Changes made by Reviewer:** none (report file only).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Caveats for the user (present regardless of the REQUIRED finding)
|
||||||
|
|
||||||
|
1. **402 may persist if the routed model is pricey:** 2048 output tokens at a high per-token price can still exceed the ~4511-token balance. Recommended: set an explicit cheaper `OPENROUTER_MODEL`, lower `AI_MAX_TOKENS` (e.g. 512), or switch `AI_PROVIDER=gemini`/`llamacpp`.
|
||||||
|
2. **Cap truncates long answers:** `--full` requests are cut at `AI_MAX_TOKENS` (default 2048); raise the var if you need long-form output.
|
||||||
|
3. **Env beats config file:** an exported `AI_MAX_TOKENS`/`AI_SESSION_TURNS` in your shell overrides `ai.env` (documented `load_env_file` semantics).
|
||||||
|
4. **Session bound is opt-in:** default stays 40 messages (20 exchanges) for backward compatibility; set `AI_SESSION_TURNS=10` for "last 5 req/response".
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
# Reviewer Re-check — Finding 1 (input-guard hardening)
|
||||||
|
|
||||||
|
**Date:** 2026-09-06
|
||||||
|
**Reviewer:** big-pickle (read-only, adversarial)
|
||||||
|
**HEAD:** 8ce5479 + uncommitted Builder changes
|
||||||
|
**Re-verifies:** `AgentsReport/reviewer/2026-09-06_ai-cost-window-review.md` → Finding 1 (REQUIRED) + Finding 2 (NOTE)
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
- **Status: ACCEPT** — all three guard sites match the exact fix spec; no findings remain.
|
||||||
|
- Guard regex `^[1-9][0-9]*$` rejects 0, -5, 010, abc, empty; legitimate values (512, 2048, 4096) pass.
|
||||||
|
- `local mt` does not shadow any existing local in either provider; jq plumbing (`--arg mt` + `tonumber`; `--argjson n`) is correct.
|
||||||
|
- Scope: only the three code files changed for F1; no unrelated diffs; jq response parsing untouched.
|
||||||
|
- Caveats: (1) runtime gate suite (`make gen/check/lint/test`) still needs Orchestrator execution; (2) live 402 repro unverified.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: openrouter.sh guard — [PASS]
|
||||||
|
|
||||||
|
**Lines 22-25 (actual):**
|
||||||
|
```
|
||||||
|
local mt="${AI_MAX_TOKENS:-2048}"
|
||||||
|
[[ "$mt" =~ ^[1-9][0-9]*$ ]] || mt=2048
|
||||||
|
body="$(printf '%s' "$body" | jq -nc --arg m "$model" --argjson msgs "$body" --arg mt "$mt" \
|
||||||
|
'{model:$m, messages:$msgs, max_tokens:($mt|tonumber)}')"
|
||||||
|
```
|
||||||
|
|
||||||
|
| Check | Result |
|
||||||
|
|-------|--------|
|
||||||
|
| Guard BEFORE jq executes | ✅ lines 22-23 precede jq on line 24 |
|
||||||
|
| `local mt` no shadow | ✅ line 15 declares `model messages system body resp code body_out errmsg` — `mt` not present |
|
||||||
|
| `$mt` used consistently | ✅ declared line 22, guarded line 23, passed `--arg mt "$mt"` line 24, consumed `($mt\|tonumber)` line 25 |
|
||||||
|
| Regex rejects 0 | ✅ `0` fails `^[1-9]…` (first char must be `[1-9]`) → falls back to 2048 |
|
||||||
|
| Regex rejects -5 | ✅ `-` fails `^[1-9]…` |
|
||||||
|
| Regex rejects 010 | ✅ `0` fails `^[1-9]…` |
|
||||||
|
| Regex rejects abc | ✅ `a` fails `^[1-9]…` |
|
||||||
|
| Regex rejects empty | ✅ empty string fails `^[1-9]…` |
|
||||||
|
| Default on unset/empty | ✅ `${AI_MAX_TOKENS:-2048}` covers both; regex re-confirms |
|
||||||
|
| Legitimate values pass | ✅ `512`, `2048`, `4096` all match `^[1-9][0-9]*$` |
|
||||||
|
| jq `tonumber` safe | ✅ only guaranteed-positive-integer strings reach `tonumber` |
|
||||||
|
|
||||||
|
## Step 2: gemini.sh guard — [PASS]
|
||||||
|
|
||||||
|
**Lines 17-22 (actual):**
|
||||||
|
```
|
||||||
|
local mt="${AI_MAX_TOKENS:-2048}"
|
||||||
|
[[ "$mt" =~ ^[1-9][0-9]*$ ]] || mt=2048
|
||||||
|
body="$(printf '%s' "$messages" | jq -c --arg mt "$mt" '{
|
||||||
|
contents: [.messages[]? | {role: (.role | gsub("assistant";"model")), parts: [{text: .content}]}],
|
||||||
|
generationConfig: {maxOutputTokens: ($mt|tonumber)}
|
||||||
|
}')"
|
||||||
|
```
|
||||||
|
|
||||||
|
| Check | Result |
|
||||||
|
|-------|--------|
|
||||||
|
| Guard BEFORE jq executes | ✅ lines 17-18 precede jq on line 19 |
|
||||||
|
| `local mt` no shadow | ✅ line 15 declares `model messages system body resp code body_out errmsg` — `mt` not present |
|
||||||
|
| Identical guard pattern to openrouter | ✅ exact same two lines |
|
||||||
|
| All regex rejection cases | ✅ same analysis as Step 1 |
|
||||||
|
| jq plumbing correct | ✅ `--arg mt` string → `tonumber` → integer in `generationConfig` |
|
||||||
|
|
||||||
|
## Step 3: session_push guard — [PASS]
|
||||||
|
|
||||||
|
**Lines 288-293 (actual):**
|
||||||
|
```
|
||||||
|
session_push() {
|
||||||
|
local messages="$1" role="$2" text="$3"
|
||||||
|
local n="${AI_SESSION_TURNS:-$MAX_SESSION_TURNS}"
|
||||||
|
[[ "$n" =~ ^[1-9][0-9]*$ ]] || n="$MAX_SESSION_TURNS"
|
||||||
|
printf '%s' "$messages" | jq -c --arg r "$role" --arg t "$text" --argjson n "$n" \
|
||||||
|
'.messages += [{role:$r, content:$t}] | .messages |= .[-$n:]'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Check | Result |
|
||||||
|
|-------|--------|
|
||||||
|
| Guard regex is `^[1-9][0-9]*$` | ✅ line 291 — matches spec, rejects leading zeros |
|
||||||
|
| Old redundant `>= 1` check removed | ✅ gone; regex alone suffices |
|
||||||
|
| Guard BEFORE jq executes | ✅ lines 290-291 precede jq on line 292 |
|
||||||
|
| `--argjson n "$n"` correct | ✅ valid JSON integer after regex; jq variable `$n` used in slice `.-$n:` |
|
||||||
|
| Fallback is `$MAX_SESSION_TURNS` | ✅ line 291, not a hardcoded literal |
|
||||||
|
| `MAX_SESSION_TURNS=40` untouched | ✅ line 25 of pos-ai confirmed |
|
||||||
|
|
||||||
|
## Step 4: Scope — [PASS]
|
||||||
|
|
||||||
|
`git diff --stat HEAD` shows exactly 6 files:
|
||||||
|
- `bin/pos-ai` — F1 session_push guard + earlier-round header/help/config
|
||||||
|
- `lib/ai-providers/openrouter.sh` — F1 guard only
|
||||||
|
- `lib/ai-providers/gemini.sh` — F1 guard only
|
||||||
|
- `DOC/AGENT_Context_Project.md`, `DOC/HOWTO.md`, `DOC/POS.md` — earlier-round docs
|
||||||
|
|
||||||
|
No other files changed. No jq response-parsing shapes altered from the earlier round.
|
||||||
|
|
||||||
|
## Step 5: Consistency across all sites — [PASS]
|
||||||
|
|
||||||
|
Both providers use the identical pattern:
|
||||||
|
```bash
|
||||||
|
local mt="${AI_MAX_TOKENS:-2048}"
|
||||||
|
[[ "$mt" =~ ^[1-9][0-9]*$ ]] || mt=2048
|
||||||
|
```
|
||||||
|
|
||||||
|
session_push uses the same regex pattern for its sibling variable:
|
||||||
|
```bash
|
||||||
|
local n="${AI_SESSION_TURNS:-$MAX_SESSION_TURNS}"
|
||||||
|
[[ "$n" =~ ^[1-9][0-9]*$ ]] || n="$MAX_SESSION_TURNS"
|
||||||
|
```
|
||||||
|
|
||||||
|
The asymmetry that existed before (session had a guard, token cap did not) is now resolved. All user-configurable numeric values that reach jq/`--argjson` are guarded with the same `^[1-9][0-9]*$` pattern.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
None. All demands from the original Finding 1 and Finding 2 are satisfied.
|
||||||
|
|
||||||
|
## Verification verified
|
||||||
|
|
||||||
|
- All three guard sites exist at exact claimed lines (FACT — direct file read).
|
||||||
|
- Guard placement before jq confirmed by line-number ordering (FACT).
|
||||||
|
- No local-name shadowing in either provider (FACT — line 15 declarations compared).
|
||||||
|
- Regex rejects all specified bad inputs (FACT — `^[1-9][0-9]*$` does not match 0, -5, 010, abc, empty).
|
||||||
|
- `MAX_SESSION_TURNS=40` at line 25 is untouched (FACT).
|
||||||
|
- No scope creep (FACT — `git diff --stat`).
|
||||||
|
|
||||||
|
## Verification unverified
|
||||||
|
|
||||||
|
- Runtime gate suite execution (make gen/check/lint/test) — Orchestrator responsibility.
|
||||||
|
- Live OpenRouter/Gemini 402 fix confirmation — requires network + API key.
|
||||||
|
|
||||||
|
## Verdict
|
||||||
|
|
||||||
|
**ACCEPT**
|
||||||
|
|
||||||
|
## Caveats
|
||||||
|
|
||||||
|
1. Orchestrator must still run `make gen && git diff --exit-code && make check && make lint` before merge.
|
||||||
|
2. Live 402 reproduction unverified (design §Testing defers to Tester).
|
||||||
@@ -488,7 +488,7 @@ All `.service` files in `systemd/` are automatically copied to `/etc/systemd/sys
|
|||||||
- `~/.config/linux_post_install/entertainment.env` — entertainment plugin defaults: weather location + `ENABLED` auto-trigger list (`plugin, interval` pairs scheduled via `pos entertainment enable/disable`, systemd user timers); auto-installed from `config/entertainment.env` by `postinstall.sh` (no clobber, template printed)
|
- `~/.config/linux_post_install/entertainment.env` — entertainment plugin defaults: weather location + `ENABLED` auto-trigger list (`plugin, interval` pairs scheduled via `pos entertainment enable/disable`, systemd user timers); auto-installed from `config/entertainment.env` by `postinstall.sh` (no clobber, template printed)
|
||||||
- `~/.config/linux_post_install/system.env` — shared "system" tool settings (loaded by `pos system health` / `pos system backup` via `load_system_env()` in `lib/common.sh`; env already exported wins over the file); template `config/system.env`
|
- `~/.config/linux_post_install/system.env` — shared "system" tool settings (loaded by `pos system health` / `pos system backup` via `load_system_env()` in `lib/common.sh`; env already exported wins over the file); template `config/system.env`
|
||||||
- `~/.config/linux_post_install/notify.env` — alerting platform selection (`NOTIFY_PLATFORM=telegram,matrix`, comma-separated = fan out); read by `lib/notify.sh`; template `config/notify.env`
|
- `~/.config/linux_post_install/notify.env` — alerting platform selection (`NOTIFY_PLATFORM=telegram,matrix`, comma-separated = fan out); read by `lib/notify.sh`; template `config/notify.env`
|
||||||
- `~/.config/linux_post_install/ai.env` — AI provider config (`AI_PROVIDER`, `AI_API_KEY` secret, `AI_MODEL`, `AI_SYSTEM_PROMPT`, plus legacy fallbacks `AI_GEMINI_API_KEY`, `AI_GEMINI_MODEL`, `OPENROUTER_API_KEY`, `OPENROUTER_MODEL`); read by `pos ai`; template `config/ai.env`, auto-installed by postinstall, edit with `pos config ai`
|
- `~/.config/linux_post_install/ai.env` — AI provider config (`AI_PROVIDER`, `AI_API_KEY` secret, `AI_MODEL`, `AI_SYSTEM_PROMPT`, `AI_MAX_TOKENS`, `AI_SESSION_TURNS`, plus legacy fallbacks `AI_GEMINI_API_KEY`, `AI_GEMINI_MODEL`, `OPENROUTER_API_KEY`, `OPENROUTER_MODEL`); read by `pos ai`; template `config/ai.env`, auto-installed by postinstall, edit with `pos config ai`
|
||||||
- `~/.bashrc` — Modified by postinstall (PATH, bash completion)
|
- `~/.bashrc` — Modified by postinstall (PATH, bash completion)
|
||||||
|
|
||||||
### Feature Flags
|
### Feature Flags
|
||||||
@@ -655,7 +655,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
|||||||
| `bin/pos-system-health` | 209 | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL |
|
| `bin/pos-system-health` | 209 | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL |
|
||||||
| `bin/pos-system-schedule` | 151 | Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently |
|
| `bin/pos-system-schedule` | 151 | Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently |
|
||||||
| `bin/pos-system-uninstall` | 517 | Remove pos toolkit binaries, services, shell integration, config, and data |
|
| `bin/pos-system-uninstall` | 517 | Remove pos toolkit binaries, services, shell integration, config, and data |
|
||||||
| `bin/pos-ai` | 705 | AI assistant: ask, chat, sessions, capture, models, providers |
|
| `bin/pos-ai` | 709 | AI assistant: ask, chat, sessions, capture, models, providers |
|
||||||
| `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
|
| `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
|
||||||
| `bin/pos-tree` | 118 | Show the pos CLI command tree: categories, commands, and subcommands |
|
| `bin/pos-tree` | 118 | Show the pos CLI command tree: categories, commands, and subcommands |
|
||||||
| `completions/pos.bash` | 314 | Dynamic bash completion |
|
| `completions/pos.bash` | 314 | Dynamic bash completion |
|
||||||
|
|||||||
+1
-1
@@ -42,7 +42,7 @@ templates (without overwriting an existing file):
|
|||||||
| `system.env` | `pos system health`, `pos system backup` | `BACKUP_SERVICE_ROOTS`, `HEALTH_BACKUP_MAX_AGE_DAYS` |
|
| `system.env` | `pos system health`, `pos system backup` | `BACKUP_SERVICE_ROOTS`, `HEALTH_BACKUP_MAX_AGE_DAYS` |
|
||||||
| `compose.env` | `pos docker compose` | `TS_AUTHKEY`, `TZ`, `DNS_SERVER`, `SERVICES_BASE` |
|
| `compose.env` | `pos docker compose` | `TS_AUTHKEY`, `TZ`, `DNS_SERVER`, `SERVICES_BASE` |
|
||||||
| `entertainment.env` | `pos entertainment *` | plugin keys (`WEATHER_LAT`…), `ENABLED` |
|
| `entertainment.env` | `pos entertainment *` | plugin keys (`WEATHER_LAT`…), `ENABLED` |
|
||||||
| `ai.env` | `pos ai` | `AI_PROVIDER`, `AI_API_KEY`, `AI_MODEL`, `AI_SYSTEM_PROMPT`, `AI_GEMINI_API_KEY`, `AI_GEMINI_MODEL`, `OPENROUTER_API_KEY`, `OPENROUTER_MODEL` |
|
| `ai.env` | `pos ai` | `AI_PROVIDER`, `AI_API_KEY`, `AI_MODEL`, `AI_SYSTEM_PROMPT`, `AI_MAX_TOKENS`, `AI_SESSION_TURNS`, `AI_GEMINI_API_KEY`, `AI_GEMINI_MODEL`, `OPENROUTER_API_KEY`, `OPENROUTER_MODEL` |
|
||||||
| `schedule.d/` | `pos system schedule` | one `<name>.env` per job: `INTERVAL`, `NOTIFY`, `MSG`, `RULE`, `COMMAND` |
|
| `schedule.d/` | `pos system schedule` | one `<name>.env` per job: `INTERVAL`, `NOTIFY`, `MSG`, `RULE`, `COMMAND` |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+3
-1
@@ -61,7 +61,7 @@ Category-less tools (`config`, `tree`) live outside any category and are documen
|
|||||||
|
|
||||||
| Command | Behavior |
|
| Command | Behavior |
|
||||||
|---------|----------|
|
|---------|----------|
|
||||||
| `pos ai ask "<prompt>"` | Sends the prompt to the active provider (default: gemini) and prints the answer text to stdout. The prompt may also be piped in via stdin when no argument is given. Runs in the persistent `default` session (`~/.local/share/linux_post_install/ai/default.json`, capped at 40 turns; `--session <name>` picks another). Terse by default: a built-in system instruction asks for commands-first minimal prose and to diagnose pasted errors/output with the fix first (`--system "<text>"` replaces it wholesale, `--full` skips it; `AI_SYSTEM_PROMPT` env/config provides a custom default). With `--last`, the output of the most recent logged pos command or captured output (tail, max 4096 chars) is appended to the question. On a tty the answer is rendered as markdown (`glow` if installed, else a built-in renderer); non-tty stdout gets the raw markdown bytes unchanged |
|
| `pos ai ask "<prompt>"` | Sends the prompt to the active provider (default: gemini) and prints the answer text to stdout. The prompt may also be piped in via stdin when no argument is given. Runs in the persistent `default` session (`~/.local/share/linux_post_install/ai/default.json`, capped at 40 turns, configurable via `AI_SESSION_TURNS`; `--session <name>` picks another). Terse by default: a built-in system instruction asks for commands-first minimal prose and to diagnose pasted errors/output with the fix first (`--system "<text>"` replaces it wholesale, `--full` skips it; `AI_SYSTEM_PROMPT` env/config provides a custom default). With `--last`, the output of the most recent logged pos command or captured output (tail, max 4096 chars) is appended to the question. On a tty the answer is rendered as markdown (`glow` if installed, else a built-in renderer); non-tty stdout gets the raw markdown bytes unchanged |
|
||||||
| `pos ai --provider openrouter ask "<prompt>"` | Same, but uses OpenRouter instead of the default Gemini provider |
|
| `pos ai --provider openrouter ask "<prompt>"` | Same, but uses OpenRouter instead of the default Gemini provider |
|
||||||
| `pos ai capture <cmd..>` | Run a command, capture its stdout+stderr to screen and to `~/.local/share/linux_post_install/last_cmd_output` for `--last`. Each capture overwrites the previous one. Returns the command's exit code |
|
| `pos ai capture <cmd..>` | Run a command, capture its stdout+stderr to screen and to `~/.local/share/linux_post_install/last_cmd_output` for `--last`. Each capture overwrites the previous one. Returns the command's exit code |
|
||||||
| `pos ai chat` | Interactive REPL with multi-turn history (the `messages[]` array is appended per turn and persisted to the session file — `default` unless `--session`); replies are rendered like `ask` on a tty; `q`/`quit`/`exit` or Ctrl+C quit, `/reset` clears the history, empty input re-prompts |
|
| `pos ai chat` | Interactive REPL with multi-turn history (the `messages[]` array is appended per turn and persisted to the session file — `default` unless `--session`); replies are rendered like `ask` on a tty; `q`/`quit`/`exit` or Ctrl+C quit, `/reset` clears the history, empty input re-prompts |
|
||||||
@@ -91,6 +91,8 @@ Backward compatibility: `pos ai gemini`, `pos ai openrouter`, and `pos ai llamac
|
|||||||
| `AI_API_KEY` | yes | — | API key for the active provider (secret — masked in `pos config ai`) |
|
| `AI_API_KEY` | yes | — | API key for the active provider (secret — masked in `pos config ai`) |
|
||||||
| `AI_MODEL` | no | per provider | Model id used by `ask`/`chat`/`models` |
|
| `AI_MODEL` | no | per provider | Model id used by `ask`/`chat`/`models` |
|
||||||
| `AI_SYSTEM_PROMPT` | no | built-in terse prompt | Custom system prompt (overrides built-in; empty to reset) |
|
| `AI_SYSTEM_PROMPT` | no | built-in terse prompt | Custom system prompt (overrides built-in; empty to reset) |
|
||||||
|
| `AI_MAX_TOKENS` | no | `2048` | Max output tokens per request (OpenRouter/Gemini cost cap) |
|
||||||
|
| `AI_SESSION_TURNS` | no | `40` | Session message cap — 2 per exchange; 10 = last 5 exchanges |
|
||||||
| `AI_GEMINI_API_KEY` | fallback | — | Legacy: Gemini API key (used when `AI_API_KEY` is empty) |
|
| `AI_GEMINI_API_KEY` | fallback | — | Legacy: Gemini API key (used when `AI_API_KEY` is empty) |
|
||||||
| `AI_GEMINI_MODEL` | fallback | `gemini-2.5-flash` | Legacy: Gemini model id (used when `AI_MODEL` is empty) |
|
| `AI_GEMINI_MODEL` | fallback | `gemini-2.5-flash` | Legacy: Gemini model id (used when `AI_MODEL` is empty) |
|
||||||
| `OPENROUTER_API_KEY` | fallback | — | Legacy: OpenRouter API key (used when `AI_API_KEY` is empty) |
|
| `OPENROUTER_API_KEY` | fallback | — | Legacy: OpenRouter API key (used when `AI_API_KEY` is empty) |
|
||||||
|
|||||||
+7
-3
@@ -3,7 +3,7 @@ set -euo pipefail
|
|||||||
# POS: ai ask — AI assistant: ask, chat, sessions, capture, models, providers
|
# POS: ai ask — AI assistant: ask, chat, sessions, capture, models, providers
|
||||||
# POS_SUBCMDS: ask chat sessions capture models providers llamacpp
|
# POS_SUBCMDS: ask chat sessions capture models providers llamacpp
|
||||||
# POS_FLAGS: --provider --model --session --system --full --last --trust --no-command-execution
|
# POS_FLAGS: --provider --model --session --system --full --last --trust --no-command-execution
|
||||||
# POS_CONFIG: ai | ai.env | AI_PROVIDER=:Provider (gemini, openrouter or llamacpp, default gemini) | @[AI_PROVIDER=gemini|] Gemini | *providers=gemini | @[AI_PROVIDER=openrouter] OpenRouter | *providers=openrouter | @[AI_PROVIDER=llamacpp] llamacpp | LLAMACPP_PORT=:Server port (default 8088) | LLAMACPP_HOST=:Bind address (default 127.0.0.1) | LLAMACPP_MODEL=:Default model path (GGUF) | LLAMACPP_CTX_SIZE=num:Context window size (default 4096) | LLAMACPP_GPU_LAYERS=num:GPU layers (-1=auto, 0=CPU, default -1) | LLAMACPP_THREADS=num:CPU threads (default: nproc) | *providers=llamacpp | @General | AI_SYSTEM_PROMPT=:Custom system prompt (overrides built-in, empty to reset)
|
# POS_CONFIG: ai | ai.env | AI_PROVIDER=:Provider (gemini, openrouter or llamacpp, default gemini) | @[AI_PROVIDER=gemini|] Gemini | *providers=gemini | @[AI_PROVIDER=openrouter] OpenRouter | *providers=openrouter | @[AI_PROVIDER=llamacpp] llamacpp | LLAMACPP_PORT=:Server port (default 8088) | LLAMACPP_HOST=:Bind address (default 127.0.0.1) | LLAMACPP_MODEL=:Default model path (GGUF) | LLAMACPP_CTX_SIZE=num:Context window size (default 4096) | LLAMACPP_GPU_LAYERS=num:GPU layers (-1=auto, 0=CPU, default -1) | LLAMACPP_THREADS=num:CPU threads (default: nproc) | *providers=llamacpp | @General | AI_SYSTEM_PROMPT=:Custom system prompt (overrides built-in, empty to reset) | AI_MAX_TOKENS=num:Max output tokens per request (default 2048; OpenRouter/Gemini cost cap) | AI_SESSION_TURNS=num:Session message cap — 2 per exchange (default 40 = 20 exchanges; 10 = last 5)
|
||||||
|
|
||||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||||
|
|
||||||
@@ -91,6 +91,8 @@ Options:
|
|||||||
Config: $CONFIG_FILE (edit with 'pos config ai')
|
Config: $CONFIG_FILE (edit with 'pos config ai')
|
||||||
AI_PROVIDER Provider to use (gemini|openrouter|llamacpp, default gemini)
|
AI_PROVIDER Provider to use (gemini|openrouter|llamacpp, default gemini)
|
||||||
AI_SYSTEM_PROMPT Custom system prompt (overrides built-in; empty to reset)
|
AI_SYSTEM_PROMPT Custom system prompt (overrides built-in; empty to reset)
|
||||||
|
AI_MAX_TOKENS Max output tokens per request (default 2048; OpenRouter/Gemini cost cap)
|
||||||
|
AI_SESSION_TURNS Session message cap — 2 per exchange (default 40 = 20 exchanges; 10 = last 5)
|
||||||
Provider keys: auto-discovered from lib/ai-providers/*.sh
|
Provider keys: auto-discovered from lib/ai-providers/*.sh
|
||||||
(AI_GEMINI_API_KEY, OPENROUTER_API_KEY, etc.)
|
(AI_GEMINI_API_KEY, OPENROUTER_API_KEY, etc.)
|
||||||
|
|
||||||
@@ -285,8 +287,10 @@ session_save() {
|
|||||||
# Append a turn and prune to the last MAX_SESSION_TURNS entries. stdout = JSON.
|
# Append a turn and prune to the last MAX_SESSION_TURNS entries. stdout = JSON.
|
||||||
session_push() {
|
session_push() {
|
||||||
local messages="$1" role="$2" text="$3"
|
local messages="$1" role="$2" text="$3"
|
||||||
printf '%s' "$messages" | jq -c --arg r "$role" --arg t "$text" \
|
local n="${AI_SESSION_TURNS:-$MAX_SESSION_TURNS}"
|
||||||
'.messages += [{role:$r, content:$t}] | .messages |= .[-'"$MAX_SESSION_TURNS"':]'
|
[[ "$n" =~ ^[1-9][0-9]*$ ]] || n="$MAX_SESSION_TURNS"
|
||||||
|
printf '%s' "$messages" | jq -c --arg r "$role" --arg t "$text" --argjson n "$n" \
|
||||||
|
'.messages += [{role:$r, content:$t}] | .messages |= .[-$n:]'
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Terminal markdown rendering (tty-only; raw bytes otherwise) ──
|
# ── Terminal markdown rendering (tty-only; raw bytes otherwise) ──
|
||||||
|
|||||||
@@ -14,8 +14,11 @@ provider_default_model() { printf 'gemini-2.5-flash'; }
|
|||||||
provider_generate() {
|
provider_generate() {
|
||||||
local model="$1" messages="$2" system="${3:-}" body resp code body_out errmsg
|
local model="$1" messages="$2" system="${3:-}" body resp code body_out errmsg
|
||||||
# Convert OpenAI messages format to Gemini contents format
|
# Convert OpenAI messages format to Gemini contents format
|
||||||
body="$(printf '%s' "$messages" | jq -c '{
|
local mt="${AI_MAX_TOKENS:-2048}"
|
||||||
contents: [.messages[]? | {role: (.role | gsub("assistant";"model")), parts: [{text: .content}]}]
|
[[ "$mt" =~ ^[1-9][0-9]*$ ]] || mt=2048
|
||||||
|
body="$(printf '%s' "$messages" | jq -c --arg mt "$mt" '{
|
||||||
|
contents: [.messages[]? | {role: (.role | gsub("assistant";"model")), parts: [{text: .content}]}],
|
||||||
|
generationConfig: {maxOutputTokens: ($mt|tonumber)}
|
||||||
}')"
|
}')"
|
||||||
if [ -n "$system" ]; then
|
if [ -n "$system" ]; then
|
||||||
body="$(printf '%s' "$body" | jq -c --arg s "$system" \
|
body="$(printf '%s' "$body" | jq -c --arg s "$system" \
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ provider_generate() {
|
|||||||
else
|
else
|
||||||
body="$(printf '%s' "$messages" | jq -c '.messages')"
|
body="$(printf '%s' "$messages" | jq -c '.messages')"
|
||||||
fi
|
fi
|
||||||
body="$(printf '%s' "$body" | jq -nc --arg m "$model" --argjson msgs "$body" \
|
local mt="${AI_MAX_TOKENS:-2048}"
|
||||||
'{model:$m, messages:$msgs}')"
|
[[ "$mt" =~ ^[1-9][0-9]*$ ]] || mt=2048
|
||||||
|
body="$(printf '%s' "$body" | jq -nc --arg m "$model" --argjson msgs "$body" --arg mt "$mt" \
|
||||||
|
'{model:$m, messages:$msgs, max_tokens:($mt|tonumber)}')"
|
||||||
resp="$(curl -sS -m 60 -X POST "https://openrouter.ai/api/v1/chat/completions" \
|
resp="$(curl -sS -m 60 -X POST "https://openrouter.ai/api/v1/chat/completions" \
|
||||||
-H "Authorization: Bearer ${AI_API_KEY}" \
|
-H "Authorization: Bearer ${AI_API_KEY}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
|||||||
Reference in New Issue
Block a user