Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 172 additions & 17 deletions docs/rtx-3080-precision.md

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions docs/serving.md
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,15 @@ does not emit the Chat Completions `[DONE]` sentinel. With tools enabled, ordina
streams immediately; only an ambiguous tool-call marker prefix or the structured tool region is
held. Tool markup that never opens a function is flushed back as ordinary text without losing bytes.

Terminal Responses also carry a local `timings` extension in both aggregate and streaming mode:
`predicted_n` is the generated token count, `predicted_ms` is Engine wall time from first through
last generated token, and `ttft_ms` is generation-request latency to the first token, excluding
host prompt preparation. Decode throughput is `max(predicted_n - 1, 0) * 1000 / predicted_ms` when
the duration is positive; the first token comes from prefill. These measurements include tokens
held by the tool parser. Clients must not measure tool-only generation from function-argument SSE
arrival times, because those events can arrive together at completion. This extension does not
change output Items, usage, or terminal status.

### Local response state and resources

`store` defaults to `true`. Stored Responses live only in this server process and are bounded by an
Expand Down
2 changes: 1 addition & 1 deletion src/serve/openai_responses_http.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ void HttpServer::handle_responses(const httplib::Request& req, httplib::Response
prepared = service_->prepare(
resolved.generation,
request.stream ? GenerationConsumerMode::Streaming : GenerationConsumerMode::Aggregate,
{}, [&req] { return client_disconnected(req); }, std::move(resolved.cache_hints));
{.phase_timings = true}, [&req] { return client_disconnected(req); }, std::move(resolved.cache_hints));
} catch (const ApiException& exception) {
const ApiError error = responses_error(exception.error());
record_request_rejected(make_request_rejection_log_context(
Expand Down
10 changes: 10 additions & 0 deletions src/serve/openai_responses_response.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "serve/openai_common.h"

#include <algorithm>
#include <cmath>
#include <iterator>
#include <stdexcept>
#include <string>
Expand Down Expand Up @@ -204,6 +205,15 @@ BuiltOpenAIResponse build_response(const std::string& id, std::int64_t created_a
{"output_tokens", outcome.completion_tokens},
{"output_tokens_details", Json{{"reasoning_tokens", outcome.reasoning_tokens}}},
{"total_tokens", outcome.prompt_tokens + outcome.completion_tokens}};
// Function calls are parsed atomically at completion. Their SSE arrival
// times cannot measure model decode speed; publish the Engine wall clock.
const auto milliseconds = [](double seconds) {
return std::isfinite(seconds) && seconds > 0.0 ? seconds * 1000.0 : 0.0;
};
response["timings"] = Json{
{"predicted_n", outcome.completion_tokens},
{"predicted_ms", milliseconds(outcome.metrics.generation_wall_seconds)},
{"ttft_ms", milliseconds(outcome.metrics.ttft_seconds)}};
built.body = std::move(response);
return built;
}
Expand Down
6 changes: 6 additions & 0 deletions tests/test_openai_responses.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ GenerationOutcome sample_outcome() {
outcome.reasoning_tokens = 3;
outcome.finish_reason = ninfer::FinishReason::StopToken;
outcome.metrics.prefix_cache_hit_tokens = 4;
outcome.metrics.generation_wall_seconds = 0.125;
outcome.metrics.ttft_seconds = 0.25;
return outcome;
}

Expand Down Expand Up @@ -1051,6 +1053,10 @@ int test_response_object() {
make_openai_response_object("resp_test", 123, request, runtime, sample_outcome());
const Json& response = built.body;
int failures = 0;
failures += check(response.at("timings").at("predicted_n") == 7 &&
response.at("timings").at("predicted_ms") == 125.0 &&
response.at("timings").at("ttft_ms") == 250.0,
"response timings preserve measured Engine generation intervals and TTFT");
// RequestJson keeps insertion order and compares objects in it; the response is built with
// sorted keys, so the expected parts are written in that order.
const Json summary =
Expand Down
108 changes: 90 additions & 18 deletions tools/dsh/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ This directory is the repository copy of the RTX 3080 Bonsai coding preset and i
Make future changes here, run the tests, and copy the preset files to the DSH installation.
The server profile remains in [`../windows/profiles.json`](../windows/profiles.json).

The six files in [`bonsai2-heretic-3080/`](bonsai2-heretic-3080/) contain the preset composition,
native NInfer adapter, prompt projection, recoverable compaction, and on-demand skill discovery.
The four JavaScript modules match the deployed implementation. The configuration derives the
The eight files in [`bonsai2-heretic-3080/`](bonsai2-heretic-3080/) contain the preset composition,
native NInfer adapter, timing projection, loop guard, prompt projection, recoverable compaction,
and on-demand skill discovery. The six JavaScript modules match the deployed implementation. The configuration derives the
optional user skill directory from `USERPROFILE` instead of embedding a particular user's path.
Credentials, complete host settings, conversation histories, context archives, and obsolete backups
are not part of this bundle.
Expand All @@ -32,25 +32,33 @@ Get-ChildItem -LiteralPath './tools/dsh/bonsai2-heretic-3080' -File | ForEach-Ob
Copy-Item -LiteralPath $_.FullName -Destination $presetDirectory -Force
}
$nativePluginUrl = ([uri](Join-Path (Resolve-Path -LiteralPath $presetDirectory).Path 'native-ninfer.mjs')).AbsoluteUri
$hostFragment = (Get-Content './tools/dsh/host.cordis.patch.example.yml' -Raw).Replace('{{NATIVE_PLUGIN_URL}}', $nativePluginUrl.Replace("'", "''"))
$statsPluginUrl = ([uri](Join-Path (Resolve-Path -LiteralPath $presetDirectory).Path 'ninfer-session-stats.mjs')).AbsoluteUri
$hostFragment = (Get-Content './tools/dsh/host.cordis.patch.example.yml' -Raw).Replace('{{NATIVE_PLUGIN_URL}}', $nativePluginUrl.Replace("'", "''")).Replace('{{STATS_PLUGIN_URL}}', $statsPluginUrl.Replace("'", "''"))
$hostFragment | Set-Content -LiteralPath (Join-Path $dshDirectory 'bonsai-host-fragment.yml') -Encoding utf8
```

Merge the generated `bonsai-host-fragment.yml` operation into the DSH home's `cordis.patch.yml`.
If it already contains `native-ninfer`, update that row instead of adding another. The example
If it already contains `native-ninfer`, update that row instead of adding another. Also apply the
`session-stats` disable operation and insert `ninfer-session-stats` once. DSH treats `name` in an
ordinary patch as a match condition, so changing the stock row's name does not replace its plugin.
The example
endpoint is `http://127.0.0.1:18020/v1`, matching the tuned local deployment; the repository switcher
uses port 8080 without an override, so set `baseURL` to the actual server port.

Merge [`settings.example.yaml`](settings.example.yaml) into the DSH home's `settings.yaml`,
preserving unrelated fields. Remove duplicate `qwen-3080` and `qwen-3080-summary` entries from
`llm-pi-ai.providers` if present: the native adapter owns these two provider names. Retain other
providers and credentials. These are configuration fragments, not replacements for the whole files.
An existing installation with the same adapter registration and defaults only needs the file copy.
Restart DSH to load the updated modules.
After the adapter and stats registrations are installed, later module updates only need the file
copy. Restart DSH to load the updated modules; use `dsh web --port 3081 --no-open` to retain the
existing browser tab without opening another.

Start NInfer with `./tools/windows/ninfer-switch.ps1 reason64`. That profile supplies the 65,536-token
minimum, FP32 recurrent state, MTP, 2,048 generated thinking tokens per step and 8,192 maximum output
tokens. The client discovers actual server capacity; its preset does not allocate GPU context.
minimum, RK4 KV, FP32 recurrent state, low thinking capped at 2,048 tokens per step, temperature 0.2,
and 8,192 maximum output tokens. It unloads the MTP draft head to make room for the more accurate
cache. The client discovers actual server capacity; its preset does not allocate GPU context.
Low thinking remains enabled; medium and off are explicit alternatives. An existing session's
selection overrides the new-session default, so select low for a session previously set to off.

## Behavior and qualification

Expand All @@ -60,9 +68,47 @@ archives with indexed references, while rejecting incomplete or tool-call-only s
preset preserves normal coding tools and their validation, bounds reads/search output, and discovers
skills on demand. Archive files have a local Git ignore rule.

The default model still makes reasoning and verification mistakes. The real long-history coding
diagnostic completed without context errors or output cutoffs, but required independent feedback to
correct its initial implementation. See the [precision and reliability report](../../docs/rtx-3080-precision.md#dsh-context-handling-and-coding-diagnostics)
Checkpoint instructions are supplied as system instructions, separately from the quoted history.
When a summary reaches its output limit on a large fragment, the compactor halves that fragment
and continues from the last complete checkpoint. All unconsumed source is retained. If large
fragments overflow twice, or a checkpoint still cannot be shortened, a large history uses bounded
whole source excerpts plus its complete indexed archive. This fallback is labeled explicitly and
never accepts a failed model summary. Small spans still fail without replacing their history when
they cannot be compacted usefully; the base engine always checks that a replacement is smaller.

When old reasoning leaves insufficient answer space, preflight first omits reasoning from earlier
assistant steps and recounts the replacement prompt. It preserves the newest assistant step's
reasoning and every user instruction, visible answer, code block, tool call, and tool result.
The stored conversation is unchanged; `omittedReasoningBlocks` records the prompt adjustment in
the response metadata. If that still cannot fit, the existing compaction path handles the error.
This avoids rejecting a final answer merely because obsolete reasoning filled the window.

The loop guard warns after two identical call/result pairs and refuses another execution after
three unchanged results for the same arguments. It allows one recovery attempt per human turn,
with earlier reasoning omitted from subsequent prompts while all instructions, visible answers,
and tool evidence remain intact. A different operation can proceed; another blocked duplicate
ends the turn. Recovery consumption and denied-call evidence survive resume and compaction,
so an ignored warning cannot produce an unlimited stream of denied calls. The last 16 distinct call signatures
are tracked separately per agent; alternating calls cannot immediately reset detection. Background
job polling and status queries are exempt. New human input resets the guard, while compaction does
not; resumed agents reconstruct evidence from the last 256 durable events. This bounds runaway
execution, but does not establish that a model has solved the task or repaired its error.

NInfer parses tool calls at completion, so their SSE arrival time cannot measure generation speed.
The adapter records the server's measured `timings` in response metadata, and the host projection
uses `max(predicted_n - 1, 0) / (predicted_ms / 1000)` for decode throughput. Invalid or absent
timing metadata on an old tool-only record contributes neither decode tokens nor decode duration;
ordinary token-usage and wall-time totals are preserved. Old records with real text/reasoning
deltas retain their observable timing. Projection version 2 invalidates old cached calculations
when the session is loaded; cold sidebar hints can remain stale until that authoritative snapshot
arrives. Conversation events are never rewritten. Build the server change as well as installing
the plugins to obtain measured timings for new tool-only responses.

The preset tells the agent to run checks on the final revision, inspect errors, and preserve
independent expected results. Its live qualification checks the resulting code independently of
the agent's final message. Incorrect first drafts still occur; passing these fixtures establishes
the tested repair workflows, not universal model accuracy. See the
[precision and reliability report](../../docs/rtx-3080-precision.md#end-to-end-coding-qualification)
for the measured results and limitations.

## Offline regression tests
Expand All @@ -72,12 +118,38 @@ workspaces; they do not read personal DSH configuration or require a loaded mode

```powershell
$env:BONSAI_LIVE_TOKEN_COUNT = '0'
node --test ./tools/dsh/tests/bonsai-local.test.mjs `
./tools/dsh/tests/native-ninfer.test.mjs `
./tools/dsh/tests/bonsai-prompt.test.mjs `
./tools/dsh/tests/bonsai-compaction-bounds.test.mjs `
./tools/dsh/tests/bonsai-compaction-recovery.test.mjs
node --test ./tools/dsh/tests/*.test.mjs
```

All 44 checks pass on the verified installation. The optional `BONSAI_LIVE_TOKEN_COUNT=1` path
The suite includes the real DSH tool executor and agent loop, server-timing folds, patch composition,
exact context limits, and compaction recovery. The optional `BONSAI_LIVE_TOKEN_COUNT=1` path
contacts the local server for token counts; it is not needed for the offline suite.

## Live accuracy qualification

Run these explicitly with the GPU server available. No personal DSH configuration is read. The
coding evaluator mounts DSH's real agent loop, adapter, prompt projection and loop guard, while
confining file tools to in-memory fixture files. Its `pwsh` tool dispatches only the independent
verifier; model output cannot execute an actual shell command or edit the oracle.

```powershell
node tools/dsh/eval/coding-agent.mjs short
$env:EVAL_CASE = 'money'
$env:EVAL_LONG_ROWS = '2300'
node tools/dsh/eval/coding-agent.mjs long
Remove-Item Env:EVAL_CASE, Env:EVAL_LONG_ROWS
node tools/dsh/eval/context-recovery.mjs
$env:EVAL_EFFORT = 'low'
$env:EVAL_TEMPERATURE = '0.2'
node tools/dsh/eval/tool-arguments.mjs tool-correction
Remove-Item Env:EVAL_EFFORT, Env:EVAL_TEMPERATURE
```

The three coding tasks cover decimal money, duration parsing and interval merging, with 315
independent cases. Success requires passing code, an observed verification of its final revision,
and a normally completed turn. The long variant inserts about 51K background tokens before the
final instruction. `EVAL_REPEATS`, `EVAL_EFFORT`, and `EVAL_TEMPERATURE` select repetitions or
controlled alternatives. Reports and fixture-only traces go under `profiles/accuracy-3080/`.
Without an effort override, `tool-arguments.mjs` compares none, medium and low; this is a diagnostic
matrix, so consult each `pass` result. Its `--reference` mode targets a separately started Prism
chat server through `NINFER_BASE_URL`; stop NInfer before loading another full GPU model.
8 changes: 8 additions & 0 deletions tools/dsh/bonsai2-heretic-3080/agent.cordis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.
Inspect relevant files, make focused changes, and verify the result. Keep tool output small
and read large files in ranges. Work in short steps; split large file writes into edits.
Treat the requested behavior as acceptance criteria. Execute relevant checks after changing code,
including invalid inputs and boundary cases. Inspect errors and fix their cause before retrying.
Keep existing independent tests intact; do not change an expected result merely to make a test pass.
Use test execution to check arithmetic and behavior instead of repeatedly predicting test results.
Claim completion only after checks pass on the final revision; otherwise state what remains unverified.
Give a concise final answer once the task is complete.

- id: agent-instructions
Expand Down Expand Up @@ -285,3 +290,6 @@
# Project concise coding schemas before the request is logged and measured.
- id: compact-prompt
name: ./compact-prompt.mjs

- id: loop-guard
name: ./loop-guard.mjs
4 changes: 2 additions & 2 deletions tools/dsh/bonsai2-heretic-3080/compact-prompt.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export const name = 'bonsai-compact-prompt';
export const inject = ['systemPrompt'];

export const descriptions = Object.freeze({
pwsh: 'Execute PowerShell in the workspace. Inspect failed commands. Use background jobs for long commands; request wider access only after a sandbox denial and approval.',
pwsh: 'Execute PowerShell in the workspace. Variables require $. Inspect stderr even when exit code is zero; correct failures before retrying. Use background jobs for long commands; request wider access only after a sandbox denial and approval.',
bash: 'Execute a shell command in the workspace. Inspect failures; use background jobs for long commands.',
read: 'Read a UTF-8 file with line numbers. Use offset and limit for small relevant ranges.',
write: 'Write a complete UTF-8 file. Read existing files first and use the same file_path spelling; preserve unrelated content.',
Expand Down Expand Up @@ -46,6 +46,6 @@ export function apply(ctx) {
ctx.on('agent/request', async (_request, next) => {
const config = await next();
if (config.provider !== 'qwen-3080' || config.model !== 'bonsai2-heretic') return config;
return { ...config, reasoningEffort: config.reasoningEffort === 'off' ? 'off' : 'medium', maxTokens: 8192 };
return { ...config, reasoningEffort: ['off', 'low', 'medium'].includes(config.reasoningEffort) ? config.reasoningEffort : 'low', maxTokens: 8192 };
});
}
Loading
Loading