Commit Graph

221 Commits

Author SHA1 Message Date
Sergey Kozyrenko 2853c97549 feat(reasoning): add three-state On/Off/Default reasoning control
Add an explicit reasoning Off to the per-agent config, backed by the langchaingo
tri-state API. Off emits llms.WithReasoningDisabled() (the provider disable wire)
via BuildOptions, and UsesAdaptiveThinking is guarded so Off overrides the
adaptive-only auto-adaptive.

Per-model capability is derived at runtime from llms.ReasoningSupportFor and
surfaced through GraphQL ModelReasoningInfo, so the UI only offers Off where it
actually disables: cannotDisable reports when Off would be rejected (always-on
models) OR a silent no-op (an unclassified default-on model whose disable wire is
omitted), and capability is surfaced for any thinking-capable model (e.g. Gemini,
which declares thinking without a reasoning block). A new
ProviderType.ReasoningProvider() supplies the provider to the resolver.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 19:50:43 +07:00
Sergey Kozyrenko ffbbabb517 fix(docker,flows): surface truncated/systemerr exec streams, bound ftester drain, tidy failure logging
- demuxExecStdout errors on a header cut short mid-frame (was a silent EOF that
  dropped the tail) and surfaces a docker systemerr frame instead of discarding it.
- ftester drains telemetry on exit through the bounded observer.Drain instead of
  two unbounded ForceFlush calls, so an unreachable collector can't hang it at exit.
- container-listing failures are sorted for a deterministic skipped-entries preview,
  logged per-entry at Debug (the detail is already in the response and the endpoint
  is hit on every navigation) with names quoted so control bytes in a hostile
  filename can't inject into a log line; corrected the stat-failure comment
  (a dangling symlink lstats fine, it doesn't fail).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:06:21 +07:00
Sergey Kozyrenko 7cd22ccbc9 test(docker,flows): cover the listing cap paths, bound the container-list path count
The truncation slice and the demux stdout byte-cap had no docker-layer tests —
only the handler's Truncated wiring was exercised through the fake, so a
mis-slice or a dropped cap would have gone unnoticed. Extract find-output
parsing into a pure parseFindEntries and take the byte cap as a demuxExecStdout
parameter, then unit-test both boundaries (at cap / cap+1 / over-limit stream).

Also bound how many paths one container-files request may list
(maxContainerListPaths), so the per-path entry cap can't be multiplied by an
attacker-chosen path count into a large fan-out or response body.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 23:22:33 +07:00
Sergey Kozyrenko 18234f05a3 docs(docker): refresh the container-listing doc, drop a stale telemetry test comment
The container-listing section still described the removed `ls -1` parse,
`pkg/queue`, and the old `[]container.PathStat` return. Update it to the current
`find -print0` + errgroup fan-out returning `ContainerDirListing{Files, Failures,
Truncated}`. Also remove the stale `WithBlock`/`DefaultDialTimeout` paragraph left
above the non-blocking `grpc.NewClient` test — its own next paragraph already
contradicted it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:45:34 +07:00
Sergey Kozyrenko 7b67b1c0ea fix(flows): return partial container listings instead of failing; stop the refetch skeleton flash
Container-listing polish on top of the partial-success work:

- A directory whose entries all fail to stat no longer 500s. The find exec
  already proved the container alive, so ListContainerDir returns the readable
  entries (possibly none) plus the per-entry failures, and the handler serves a
  200 partial listing; only a cancelled request or a dir that can't be listed at
  all still errors.

- Over the entry cap, list the first page and set a Truncated flag rather than
  erroring with end-user copy from the docker layer. The flag flows through to
  the Pull dialog, which now warns the user the directory was truncated instead
  of silently showing a subset.

- The Pull dialog no longer flashes a skeleton on an in-place refetch (Refresh,
  or after a Pull): the listing hook clears its rows only when the path actually
  changes, so a same-path reload keeps the current rows under the loading guard.

Tests: an all-entries-failed listing returns 200 with the failures; the
Truncated flag surfaces in the response. Swagger regenerated for the new field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:38:17 +07:00
Sergey Kozyrenko 2c524faea9 fix(observability,flows): drain telemetry sinks in parallel, stop leaking docker errors, dedup listing failures
Three defects on the shutdown and container-listing paths:

- Telemetry drain ran the langfuse and otel sinks serially under one deadline.
  langfuse's ForceFlush/Shutdown ignore the caller ctx and block on their own
  timeout, so a langfuse outage burned the whole budget and the healthy otel
  sink's final batch was never flushed. Add observer.Drain, which flushes then
  shuts down each sink in its own goroutine bounded by the deadline; main uses
  it, retiring the inline flush-then-shutdown dance.

- The partial-success listing put the raw stat/list error into the 200 body's
  Failures[].Message, exposing docker-layer detail (container ids, the daemon
  address) that response.Error only reveals under develop mode. Send a generic
  message to the client and keep the raw text dev-gated, logged server-side.

- A path read successfully by one query could also appear in Failures when
  another query failed on it, order-dependent. Reconcile at assembly: drop any
  failure whose path was read successfully, so no path is in both arrays.

Also bound the exec stdout buffered before parsing so a compromised sandbox
can't stream unbounded output into memory.

Tests: Drain delivers a healthy otel sink while a langfuse sink blocks and
returns within its deadline; the listing failure message is dev-gated; a path
never lands in both Files and Failures. Live: unreachable collector -> shutdown
stays bounded (~5s) and exits cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:24:02 +07:00
Sergey Kozyrenko bc8714db6e fix(observability): flush telemetry on graceful shutdown, drain every sink
The graceful-shutdown branch only logged "cleaning up resources" and exited
without flushing telemetry, so the last buffered batch was dropped on every
SIGTERM. Drain it, with three corrections:

- main: flush then shut down the observer on shutdown, bounded by a 5s
  deadline in a goroutine so an unreachable collector can't stall exit.
  Flush must precede Shutdown because the langfuse observer's Shutdown
  cancels its context without flushing; its final batch drains only via
  ForceFlush while the context is still live.
- observer Flush/Shutdown: drain both the langfuse and otel sinks and join
  their errors instead of returning after the first, which skipped the
  second sink entirely when both were configured.
- telemetryClient Flush/Shutdown: attempt all three providers and always
  close the grpc connection, joining errors, so a stalled provider flush
  no longer skips the rest or leaks the connection.

Tests: the langfuse observer test proves Shutdown drops the buffered batch
while ForceFlush drains it to the sink; observer-level tests prove both
sinks drain and errors aggregate. Live: with an unreachable collector,
shutdown stays bounded (~5s) and exits cleanly instead of stalling ~10s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 17:30:31 +07:00
Sergey Kozyrenko c0660b8398 fix(observability): connect the telemetry collector lazily, tear down cleanly
The startup dial used grpc.WithBlock with a 10s timeout, so a set-but-
unreachable collector stalled main() for up to 10s, and once that dial failed
the client was nil forever — a collector that came up later never connected
without a restart. Switch to grpc.NewClient (non-blocking): startup never
stalls and gRPC re-establishes the connection in the background on its own.
Also build all three exporters before any provider, so the exporter-error
cleanup (closing the connection) happens before any batch/reader goroutine
starts and can't leak one. Drops the deprecated WithBlock/WithReturnConnectionError.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 16:45:41 +07:00
Sergey Kozyrenko 345b91e4c1 docs(swagger): regenerate for the container-files Failures field
ContainerFiles gained a Failures list when the container listing switched to
partial-success; regenerate the Swagger docs (swag v1.8.7, the pinned version)
so generated clients see the new field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 16:37:18 +07:00
Sergey Kozyrenko a306b156a1 chore: gitignore backend/vendor, drop stale pkg/queue doc row, self-contained test messages
- Ignore the local `backend/vendor/` tree (187 MB, present only for the
  vendored-fork build) so it can't be accidentally staged.
- Remove the architecture-table row for `pkg/queue/`, which was deleted when
  the container stat fan-out moved to errgroup.
- Drop the internal "H4" report-id labels from the telemetry test — the
  sentences already say what they guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 16:37:17 +07:00
Sergey Kozyrenko 780c2a722e fix(docker): drop the TTY from the dir listing to keep newline filenames intact
find -print0 already delimits with NUL, but the exec still ran under a TTY,
whose onlcr translation rewrites every \n in the stream to \r\n — including a
\n that is part of a filename — so a file literally named with a newline was
stat'd under the mangled name and reported unreadable. Run the exec without a
TTY and demux the resulting multiplexed stdout/stderr stream by hand (no new
dependency), so filename bytes reach the parser untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 16:20:36 +07:00
Sergey Kozyrenko 93d36c9827 fix(flows): keep serving readable paths when one container path fails
The multi-path container listing still fail-fasted: a single bad top-level
path (nonexistent, not a directory, list failed) returned HTTP 500 and
blanked every path that had already listed. Record each such path as a
failure and keep going so the readable paths are still returned; only a
cancelled request aborts, and if every path fails at the directory level the
request is failed as a whole. Also drop a per-entry failure whose path was
already read successfully by an overlapping query.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 16:12:40 +07:00
Sergey Kozyrenko ac9a16517a fix(docker): list container dirs with find -print0, not ls parsing
Parsing `ls -1` output was unsafe: under the TTY the exec allocates, GNU
coreutils shell-quotes names — so a readable file with a space, quote or
non-ASCII byte was stat'd with the quotes and reported unreadable — and
busybox wraps every entry in ANSI colour codes, so on an alpine/busybox
image the whole listing came back unreadable. Switch to `find -maxdepth 1
-print0`: literal bytes, portable across GNU and busybox, and the NUL
delimiter survives filenames containing newlines. Cap the entry count so a
directory with hundreds of thousands of files can't fan out into that many
Docker API calls, and classify a cancelled request or an all-entries-failed
listing as a directory-level error instead of a misleading empty 200.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 16:12:38 +07:00
Sergey Kozyrenko 1300f60a2b feat(docker): partial-success container listing instead of fail-fast
A single unreadable directory entry (dangling symlink, a file removed
between ls and stat, a transient /proc entry) used to fail the whole
listing with HTTP 500, blanking the file browser and discarding every
readable sibling. The frontend already expects /proc/sys to not fail
spuriously, but the backend did the opposite.

ListContainerDir now returns a ContainerDirListing{Files, Failures}: per-
entry stat errors no longer abort the batch. GetFlowContainerFiles serves
the readable entries as HTTP 200, carries the failures back in a new
ContainerFiles.Failures field, and logs each skipped entry (capped) plus a
degradation summary. Directory-level faults (not a dir, ls failed, container
gone) still return 500 — there is no partial to show. statContainerEntries
returns successes + failures instead of the lowest-index error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 13:32:48 +07:00
Sergey Kozyrenko 8212802c84 fix(docker): clamp non-positive worker count in the stat fan-out
The deleted queue normalized workers<=0 to a default; the errgroup helper
passed the count straight to SetLimit, where 0 deadlocks the first Go() on an
unbuffered semaphore and a negative value disables the bound entirely. The
sole caller hardcodes 20 so this was latent, but clamp it back so a future
config-driven count can't hang the listing or flood the Docker daemon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 03:03:57 +07:00
Sergey Kozyrenko 40eeea93c0 refactor(docker): replace the custom queue with a bounded errgroup fan-out
ListContainerDir was the only consumer of pkg/queue and discarded its ordered
delivery (the caller dedups and re-sorts). Replace the fan-out with a small
errgroup helper — SetLimit keeps the same 20-way bound on concurrent stats
against the Docker daemon, the caller's context propagates into each call,
results stay in input order, and the lowest-index failure is surfaced
deterministically — then delete pkg/queue. The helper was verified byte-
identical to the previous queue implementation across 600 randomized directory
shapes (sizes, failure positions, out-of-order completion) before removal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 02:05:24 +07:00
Sergey Kozyrenko d44a33f5dd test(observability): cover the telemetry success path
Adds a fake OTLP collector so NewTelemetryClient can be exercised against a
reachable endpoint: it builds all three providers and both ForceFlush and
Shutdown export and tear down without error. Previously only the
not-configured and unreachable-dial paths were covered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 10:02:58 +07:00
Sergey Kozyrenko 745c8ae6d6 test(queue): cover the default-workers, nil-process, and Instance paths
Adds scenarios for a non-positive worker count (must fall back to the
default, else nothing drains the queue and delivery hangs), a nil process
function (logs and drops each item without panicking), and a stable
instance id. Brings queue.go to full statement coverage and kills the
mutants that previously survived on the workers<=0 fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 10:02:56 +07:00
Sergey Kozyrenko d49e740821 test(observability): cover dial-timeout + ErrNotConfigured; close conn on exporter error
Unit coverage for NewTelemetryClient: an unset OTEL_HOST yields ErrNotConfigured,
and an unreachable collector returns within the dial timeout instead of hanging
(a TCP-accepting, silent listener drives the WithBlock path).

Also close the grpc.ClientConn on the exporter-creation error paths — a
successful dial followed by a failed exporter New() previously leaked it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 09:09:38 +07:00
Sergey Kozyrenko 62a1445d32 refactor(queue): drop the running-context for a flag; reject restart-without-stop
The two-context design in ddd3916 was correct but fragile: the field named
`ctx` was the liveness signal goroutines must NOT bail on, while the real
cancellation signal was `stopCtx`. That inversion is the exact trap that
produced the earlier happy-path regression (bailing on `ctx`, which a normal
input-close cancels, dropped in-flight results). It also left a latent
restart-without-Stop hazard: a second Start() after input-close orphaned the old
stopCtx and shared wg, deadlocking a later Stop() (with a data race).

Collapse `ctx` into a plain `running` bool (it was only ever read via .Err(),
never awaited), keep `stopCtx` as the single cancellation signal, and guard
Start() on stopCtx so a restart requires a prior Stop() — removing both the
naming trap and the restart hazard.

Comprehensive tests (queue_scenarios_test.go): boundary-N full delivery,
contiguous-prefix-on-abandon, a randomized prefix-invariant fuzz, goroutine-leak,
restart-reject, process-error, double-stop, Running() transitions. The happy-path
tests hang on the pre-rework code and pass here (side-by-side under -race). Live:
a flow's non-empty container dirs of 1/40/100/500 files each deliver every entry;
the error path returns 500 without hanging.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 09:09:36 +07:00
Sergey Kozyrenko ddd3916e9c fix(queue): only hard-stop workers on Stop(), not on normal input-close
The previous fix (97e5730) had workers and the reader bail on q.ctx, but the
reader also cancels q.ctx on a normal input-close — so the ListContainerDir
happy path regressed: after input closed (buffer names, close, read all N),
workers dropped still-undelivered results and the consumer hung waiting for the
last one. Any non-empty container directory that didn't error early hung the
file-manager request.

Split the signals: q.ctx still tracks "running" (cancelled by input-close or
Stop), and a new q.stopCtx is cancelled only by Stop(). Workers and the reader
bail on q.stopCtx, so a normal input-close drains and delivers every result
while a hard Stop() still unblocks a consumer that abandoned output. Stop()'s
already-stopped guard now checks q.stopCtx (input-close alone must not
short-circuit it, or blocked workers leak).

Tests: DeliversEveryResultAfterInputClose (hangs on the old fix, verified
side-by-side under -race) + StopHardStopsAfterInputClose; deadlock + ordering
still green under -race.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 00:02:51 +07:00
Sergey Kozyrenko d6fa58e5d4 fix(observability): don't let an unreachable telemetry collector down the app
A set-but-unreachable OTEL collector hung startup: NewTelemetryClient dialed
with grpc.WithBlock() and no timeout on the deadline-free signal context, so
the process blocked forever before it ever served. And a non-ErrNotConfigured
init error from either observability client was log.Fatalf, killing the
process. Both let an OPTIONAL integration take the whole app down.

Bound the dial with a 10s timeout, and on init failure degrade to a no-op
observer with a logged warning instead of exiting. Verified live: a bad
OTEL_HOST that hung startup indefinitely now boots in ~14s with a warning and
runs a full flow; a reachable collector still boots in ~4s with no warning.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 23:20:43 +07:00
Sergey Kozyrenko 97e5730a53 fix(queue): don't deadlock Stop() when the consumer stops reading output
ListContainerDir aborts on the first stat error and stops reading the queue's
output channel, leaving workers blocked on the unbuffered `q.output <- result`
send and the reader blocked on a full `q.queue`; Stop() -> wg.Wait() then hung
forever. Select every pipeline send/wait on q.ctx so a stopped queue unwinds.
On stop a worker returns without msg.cancel(), so later workers also bail via
q.ctx and output ends at a contiguous prefix instead of developing gaps.

Regression test drives the exact hang (unread output -> Stop must return),
verified with -race.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 21:29:07 +07:00
Sergey Kozyrenko 9876c13ea8 docs(providers): restore the DeepSeek legacy reasoning-format contract
The openai-compat consolidation dropped the comment warning that DeepSeek needs
the legacy top-level "reasoning_effort" string form; without it a maintainer
could add openai.WithModernReasoningFormat() to the shared opts and silently
break DeepSeek thinking mode. Restore it at the shared opts choke point.

Also fix two stale pointers: glm/qwen config.yml cited
WithPreserveReasoningContent() "in glm.go/qwen.go", but it moved to
openaicompat.go during that same consolidation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 17:02:21 +07:00
Sergey Kozyrenko f5f7974826 test(validation): cover REST prompt validation and knowledge length limits
- prompts_test.go: PatchPrompt rejects a syntax error, an undeclared variable,
  and a whitespace-only template over REST (the path a raw client hits when the
  UI is bypassed) and does not persist them; valid templates still create/update.
- validation_test.go: validateKnowledgeFieldLengths accepts each field at its
  max length and rejects one character over.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:34:04 +07:00
Sergey Kozyrenko 72da224033 feat(prompts): validate template syntax on the REST update endpoint
The GraphQL createPrompt/updatePrompt mutations run validator.ValidatePrompt
(Go text/template parse + declared-variable check + trial render), but the
REST PUT /prompts/:type handler only checked the field was present, so a
prompt with a syntax error or an undeclared variable could be stored over
REST and later break rendering. Mirror the GraphQL check in PatchPrompt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:01:36 +07:00
Sergey Kozyrenko 0cf85ef029 feat(graphql): validate knowledge and API-token mutation inputs
The REST handlers reject empty or oversized knowledge fields and over-long
API-token names through their request-model validate tags, but the GraphQL
mutations — the path the web UI uses — accepted them unchecked, so the same
entity could be stored past its documented limits depending on the caller.

Mirror the REST caps at the resolver boundary:
- createKnowledgeDocument / updateKnowledgeDocument: require content (and
  question on create), and cap content/question/description/codeLang lengths.
- createAPIToken / updateAPIToken: cap the token-name length.

Limits are kept in sync with server/models via mirror comments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:44:19 +07:00
Sergey Kozyrenko bcef80de99 fix(graph): reject empty flow title, flow input, and assistant input
The renameFlow / putUserInput / callAssistant GraphQL resolvers passed the
title/input straight to the controller with no non-empty check, while the
equivalent REST handlers reject them with 400. An empty flow title in
particular then fails the Flow model's `required` invariant and breaks the
REST GET /flows listing for that user. Mirror the REST guard at the resolver
boundary, matching the existing createFlow "... is required" checks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:16:33 +07:00
Sergey Kozyrenko e1b7d8e5d9 fix(resources): pre-flight blob stats so a ZIP download can't truncate under 200
The streaming ZIP refactor made `ZipResources` open each blob inline while
`streamZipArchive`'s writer commits HTTP 200 on the first byte. A blob missing
on disk (a DB record whose blob file is gone) failed mid-stream, so the client
received a 200 with a central-directory-valid but incomplete archive — the
missing files silently dropped (regression vs main, which buffered then sent).

Stat every blob up front; a missing one now returns before any byte is written,
so `streamZipArchive` emits a clean structured error instead. Keeps the
streaming memory benefit.

Tests (both proven fail-on-unfixed / pass-on-fixed): a unit test asserts the
writer stays empty when a blob is missing, and a download-handler test asserts
a missing blob in a multi-file ZIP returns a clean 500, not a truncated 200.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 13:51:54 +07:00
Sergey Kozyrenko d3c46c7158 test: close the coverage gaps flagged by the review
- providers: prove a stale user-provider row does not knock out a valid USER
  sibling (TestGetProviders_StaleUserRowSpansValidSibling — ollama builds keyless
  so it survives beside a skipped minimax; side-by-side verified).
- settings-provider: cover the create-form ?type=/?id= guards (disabled/unknown
  type and clone-of-disabled bounce to the list; an enabled type renders). These
  had zero coverage — a swap/drop-return regression would have shipped green.
- knowledge-form: cover performSave's server-document reset branch (untouched
  fields reflect the returned document under keepDirtyValues), and the useBlocker
  "Save and leave" path via a real data router (proceeds the blocked nav, does
  NOT honor a CREATE redirect). Swap the negative no-navigate assertion's
  setTimeout(0) flush for a deterministic Save-disabled anchor.

Each test mutation-verified to fail on the reverted production code; 652 frontend
tests + go test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 00:55:29 +07:00
Sergey Kozyrenko 84d87cf474 refactor(providers): apply review follow-ups (degrade log, count helper, empty menu)
From a strict re-review of the recent commits (no blocker/high/security found):
- providers.go GetProviders: the skip covers ANY unbuildable saved provider, not
  only a disabled type — drop the misleading "of unavailable type" wording and
  lower the line to Debug (it re-fires on every providers fetch; WithError keeps
  the reason).
- settings-prompt countVariableUses: drop the redundant seed-in-map side effect;
  the component already falls back to `?? 0` for unused variables, so the loop's
  own `?? 0` is the only seed needed.
- settings-providers create menu: render a disabled "No available provider types"
  placeholder instead of a silently-empty menu (loading / no-keys / failed-query
  states), with a test covering it.

Counts live-verified unchanged (AgentType 1->3 on the stand); go test + 646
frontend tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 23:45:36 +07:00
Sergey Kozyrenko 08a24c9abd fix(providers): skip unavailable user providers instead of failing the whole list
A saved user provider whose type is no longer enabled (e.g. its API key was
removed) made GetProviders return an error for the ENTIRE `providers` query —
one stale row blocked all flow creation in the UI ("No available providers").
Skip and log such rows, mirroring how startup already tolerates disabled
default providers. Pre-existing robustness gap, not introduced by this branch.

Verified live on the docker stand: the `providers` query went from a hard
error to returning all 10 enabled providers (the stale minimax row skipped),
and flow creation works again. Adds a side-by-side regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 22:10:40 +07:00
Sergey Kozyrenko a1908536b7 test(providers): extend the price-consistency guard to all 9 providers
anthropic, bedrock, and gemini also carry per-agent prices in config.yml but
were not covered. Adding them confirms no current drift and guards them going
forward. bedrock's loaders take a *config.Config, so they're wrapped with an
empty config to read the embedded catalog.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 13:55:26 +07:00
Sergey Kozyrenko 4ecf2bbac1 fix(providers): correct model prices from a full official-source audit
Audited all 133 catalog models across 9 providers against official pricing
pages, each delta independently re-verified. openai/anthropic/gemini/glm/kimi
were already correct (0 changes). Corrections:

- deepseek-v4-pro: 1.74/3.48/0.0145 -> 0.435/0.87/0.003625 (catalog was 4x the
  official DeepSeek rate).
- bedrock mistral-large-3-675b: 4.0/12.0 -> 0.5/1.5 (8x overstatement; old
  Mistral Large 24.07 rate).
- minimax M3 0.6/2.4 -> 0.3/1.2, M2.7 0.4/1.1 -> 0.3/1.2, M2.7-highspeed
  0.4/1.1 -> 0.6/2.4, all +0.06 cache (permanent-50%-off effective rates).
- qwen3.6-35b-a3b: 0.248/1.485 -> 0.375/2.25 (catalog had the China-mainland
  price, not the International endpoint the rest of the catalog uses).
- qwen: add cache_read = 0.20 x input (implicit-cache rule) to 16 open-source
  models that were missing it.

config.yml agent prices synced for deepseek-v4-pro and MiniMax-M3 (guard test
keeps them aligned).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 13:44:59 +07:00
Sergey Kozyrenko dad1be429e feat(providers): declare reasoning efforts for gpt-5.2-codex and gpt-5-pro
Verified per the official OpenAI model pages:
- gpt-5.2-codex: [low, medium, high, xhigh]
- gpt-5-pro: [high] (the page states it "only supports reasoning.effort: high")

Other openai reasoning models (gpt-5, gpt-5.1 = no xhigh; gpt-5-mini/nano,
codex variants, o-series) keep the default — their official pages do not
enumerate an accepted effort set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 13:44:58 +07:00
Sergey Kozyrenko ebeddf4e76 fix(providers): set qwen cache_read to the documented 20% implicit-cache rate
Alibaba Model Studio's context-cache doc bills cached input tokens at 20% of the
input price (implicit hits, International endpoint). Most of the qwen catalog
already used 20%; align the outliers and guard against future drift.

- qwen3.7-max 1.25 -> 0.5, qwen3.7-plus 0.2 -> 0.08: corrects an earlier change
  that read a "50% discount" off the model-pricing page; the authoritative rate
  is 20%, so the original 0.5 for qwen3.7-max was already correct.
- qwen3.6-{max-preview,plus,flash}, qwen3.5-{plus,flash}: 10% -> 20% stale
  outliers, with matching config.yml agent-price updates.

Add TestAgentConfigPricesMatchCatalog: GetPriceInfoForType returns the agent
price with no catalog fallback, so config.yml drift silently mis-prices cost.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 05:04:13 +07:00
Sergey Kozyrenko 2f9e86391e feat(providers): declare reasoning efforts for the remaining xhigh openai models
Verified per the OpenAI model pages (reasoning.effort support):
- gpt-5.4-mini, gpt-5.4-nano, gpt-5.2: [low, medium, high, xhigh]
- gpt-5.2-pro: [medium, high, xhigh] (no low)

gpt-5 / gpt-5.1 keep the default [low, medium, high] (no xhigh; gpt-5 uses
"minimal", not representable in the UI enum). Codex variants are unverified
and left for a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 05:04:12 +07:00
Sergey Kozyrenko de7db21448 feat(providers): declare reasoning efforts so xhigh/max surface in the UI
The settings effort dropdown is driven by a model's reasoning.efforts and
falls back to [low, medium, high] when absent, so xhigh/max were never
selectable for OpenAI-compatible models even though the backend accepts them.

Declare the verified accepted levels:
- glm-5.2: [high, max] (Z.AI: GLM-5.2 reasoning_effort accepts only high/max,
  so the default low/medium were also wrong for it)
- gpt-5.4: [low, medium, high, xhigh] (OpenAI GPT-5.4/5.5 effort enum)

Other openai/gpt-5.x entries still need per-model effort verification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 04:39:31 +07:00
Sergey Kozyrenko 8bbbdbf7c5 fix(providers): correct qwen3.7-max cache_read to the official rate
cache_read was 0.5; the Alibaba Model Studio official price (International)
is 1.25 per 1M tokens (a 50% discount on the 2.5 input rate). Pre-existing
inaccuracy, unrelated to the qwen3.7-plus addition.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 03:56:04 +07:00
Sergey Kozyrenko b40091d68f fix(providers): correct kimi-k2.7-code and qwen3.7-plus prices to official rates
Replace the placeholder estimates with verified list prices (USD per 1M tokens):
- kimi-k2.7-code: 0.95 / 4.00 / 0.19 cache (platform.kimi.ai official) —
  identical input/output to k2.6, only the cache rate differs.
- qwen3.7-plus: 0.4 / 1.6 / 0.2 cache (Alibaba Model Studio official,
  International base <=256K tier) — the earlier estimate conflated it with
  the qwen3.7-max tier and was ~2.5x too high.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 03:54:31 +07:00
Sergey Kozyrenko f489e13ed4 docs(providers): drop unverifiable superlative from glm-5.2 description
"the only GLM model that honors reasoning_effort" is an unenforceable
prose claim; state the capability the entry actually carries instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 03:47:13 +07:00
Sergey Kozyrenko c3504b1839 test(evidence-receipts): cover unterminated and oversized tail reads
The tail reader's no-trailing-newline branch (a torn/truncated final
append, the case M3's fsync defends) and the over-window error branch
had no coverage. Add cases for a newline-free last line, a torn final
append that must be rejected, and a single line exceeding the 64KiB
window.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 03:47:13 +07:00
Sergey Kozyrenko 35c77ae5aa test(providers): scan the whole catalog for adaptive modes in the M7 pin
The M7 pin only checked models actually assigned to an agent type, so an
adaptive reasoning mode declared on an unassigned catalog model would pass
silently. Add a second loop over every catalog model so any openaicompat
provider that declares adaptive/adaptive-only fails loudly — matching the
guard's stated intent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 03:47:12 +07:00
Sergey Kozyrenko 83bff62d3b refactor(evidence-receipts): drop change-narration comment on the lock pool
The comment narrated the replaced sync.Map design and defended the
choice; the fixed-stripe array plus FNV indexing is self-evident and
the rationale already lives in commit d12b984.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 03:32:02 +07:00
Sergey Kozyrenko 2d8bce7ccb feat(providers): add glm-5.2, kimi-k2.7-code, qwen3.7-plus to model catalogs
- glm/models.yml: add glm-5.2 (only GLM model supporting reasoning_effort=high/max)
- kimi/models.yml: add kimi-k2.7-code (code-specialist above k2.6)
- qwen/models.yml: add qwen3.7-plus (cost-efficient tier below qwen3.7-max)

Prices for kimi-k2.7-code and qwen3.7-plus are estimated from adjacent
catalog entries and should be verified against official pricing pages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 03:11:20 +07:00
Sergey Kozyrenko a69e19f751 feat(providers): allow xhigh/max reasoning_effort on the OpenAI-compatible path
The per-agent options builder only emitted WithReasoning for low/medium/high,
and the langchaingo fork clamped xhigh/max to high in GetEffort (treating them
as adaptive-only). OpenAI GPT-5.5 and GLM-5.2 now expose xhigh/max as real
reasoning_effort levels, so that clamp encodes a stale invariant.

Extend the effort switch to pass xhigh/max through; the fork's GetEffort clamp
is removed in the local langchaingo (propagated via vendor) so the OpenAI
transport emits the real level. Adaptive (anthropic/bedrock) and budget paths
are unaffected — GetEffort is called only by the OpenAI transport. Validity is
gated per model by ModelReasoningInfo.Efforts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 02:18:57 +07:00
Sergey Kozyrenko 40debd150d test(providers): pin openaicompat providers off adaptive thinking (M7)
Adaptive thinking (thinking.type=adaptive + output_config.effort) is an
Anthropic-protocol mode the langchaingo OpenAI transport cannot emit, and
the openaicompat Call path never threads PrepareAdaptiveCallOptions. So an
adaptive reasoning mode declared in an openaicompat provider's models.yml
would silently no-op.

Assert that none of the openaicompat-backed providers (qwen, glm, deepseek,
kimi, minimax) resolve to adaptive thinking for any agent type. The day
someone adds an adaptive model/mode to one of their configs, this fails
loudly instead of dropping the thinking budget in silence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 00:52:49 +07:00
Sergey Kozyrenko 4d25cf5c51 perf(evidence-receipts): read only the file tail for the previous hash
record() called readLastEvidenceReceiptHash on every append, which re-read
and re-hashed the entire receipts.jsonl just to get the last hash and verify
the whole chain — O(N^2) over a flow's receipts, on the tool-call hot path
under the per-path lock (M2).

Read only the last line instead (windowed ReadAt from the end), failing
closed if that line is missing, wrong-schema, or hash-mismatched. This is
O(1) per append and stays correct across the multiple recorder instances
that write one path (L6): the file stays the single source of truth, so
there is no per-instance cache to go stale. Full-chain verification belongs
in a separate read-time verify tool, not on every write.

Off by default (EVIDENCE_RECEIPTS_ENABLED=false). Measured: 1024 concurrent
appends under -race dropped from ~49s to ~7.5s.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 21:43:19 +07:00
Sergey Kozyrenko 4b9e4b4972 fix(evidence-receipts): fsync each receipt append for durability
The append wrote the receipt line and returned without flushing, so a crash
between the write and the OS flushing its page cache could lose or truncate
the last receipt, after which the fail-closed chain check halts all further
writes until the file is repaired (M3). Sync the file before returning.

Off by default (EVIDENCE_RECEIPTS_ENABLED=false), so standard deployments are
unaffected; the cost lands only when the feature is explicitly enabled.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 21:21:07 +07:00
Sergey Kozyrenko 09383063ac test(evidence-receipts): pin sharded lock pool bounds and concurrency
Three guards for the L6 sharded mutex pool (d12b984):
- bounded: 100k distinct flows resolve to <= 256 mutexes, so reverting to a
  per-path map (the original unbounded leak) fails the test.
- high concurrency: 16 cross-executor writers on one path keep the hash chain
  intact under -race.
- stripe collision: two flows that hash to the same stripe keep their own
  chains intact, confirming shared stripes add contention but not corruption.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 21:00:27 +07:00