Spaces:
Running
Running
Release v1.1.4 manifest progress fix
Browse filesCorrect manifest progress accounting and document verified Safari and Firefox gates; Windows hardware gates remain open.
- README.md +7 -3
- dist/assets/{index-DIzN0g8r.js → index-Dn6W9Rfz.js} +0 -0
- dist/index.html +1 -1
- src/app/App.test.ts +15 -1
- src/app/App.tsx +11 -0
README.md
CHANGED
|
@@ -58,9 +58,13 @@ shard from byte zero; the UI does not claim resumable Range downloads.
|
|
| 58 |
27B is intentionally gated. It is unavailable when the adapter's
|
| 59 |
`maxStorageBufferBindingSize`, `shader-f16`, browser quota, or all-WebGPU graph
|
| 60 |
requirement does not pass. Chrome on Apple Silicon is the validated release
|
| 61 |
-
path.
|
| 62 |
-
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
## Growth benchmark
|
| 66 |
|
|
|
|
| 58 |
27B is intentionally gated. It is unavailable when the adapter's
|
| 59 |
`maxStorageBufferBindingSize`, `shader-f16`, browser quota, or all-WebGPU graph
|
| 60 |
requirement does not pass. Chrome on Apple Silicon is the validated release
|
| 61 |
+
path. Safari 26.5 on Apple Silicon has also passed a real foreground cold 27B
|
| 62 |
+
load through compat/Asyncify plus explicit OPFS eviction. Firefox gating was
|
| 63 |
+
validated on macOS: it selected compat, offered CPU-WASM for dense tiers, and
|
| 64 |
+
refused 27B without WebGPU; Firefox model generation was not exercised.
|
| 65 |
+
Windows Chrome/Edge on a discrete GPU and a physical low-limit iGPU laptop
|
| 66 |
+
remain explicit release-matrix gaps. Dense CPU fallback is offered only when
|
| 67 |
+
the browser exposes the required cross-origin-isolated WASM features.
|
| 68 |
|
| 69 |
## Growth benchmark
|
| 70 |
|
dist/assets/{index-DIzN0g8r.js → index-Dn6W9Rfz.js}
RENAMED
|
The diff for this file is too large to render.
See raw diff
|
|
|
dist/index.html
CHANGED
|
@@ -10,7 +10,7 @@
|
|
| 10 |
content="Bonsai local inference field station — private WebGPU language models in your browser."
|
| 11 |
/>
|
| 12 |
<title>Bonsai Field Station</title>
|
| 13 |
-
<script type="module" crossorigin src="/assets/index-
|
| 14 |
<link rel="stylesheet" crossorigin href="/assets/index-C-2tm91j.css">
|
| 15 |
</head>
|
| 16 |
<body>
|
|
|
|
| 10 |
content="Bonsai local inference field station — private WebGPU language models in your browser."
|
| 11 |
/>
|
| 12 |
<title>Bonsai Field Station</title>
|
| 13 |
+
<script type="module" crossorigin src="/assets/index-Dn6W9Rfz.js"></script>
|
| 14 |
<link rel="stylesheet" crossorigin href="/assets/index-C-2tm91j.css">
|
| 15 |
</head>
|
| 16 |
<body>
|
src/app/App.test.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
| 1 |
import { describe, expect, it } from 'vitest';
|
| 2 |
import { EngineClientError } from '../engine';
|
| 3 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
function engineError(code: string): EngineClientError {
|
| 6 |
return new EngineClientError({ code, message: code });
|
|
@@ -26,6 +32,14 @@ describe('App release defaults', () => {
|
|
| 26 |
});
|
| 27 |
});
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
describe('App streamed completion telemetry', () => {
|
| 30 |
it('uses observed stream events when final engine usage is incomplete', () => {
|
| 31 |
expect(reconcileCompletionTokens(5, 64)).toBe(64);
|
|
|
|
| 1 |
import { describe, expect, it } from 'vitest';
|
| 2 |
import { EngineClientError } from '../engine';
|
| 3 |
+
import {
|
| 4 |
+
DEFAULT_MODEL_ID,
|
| 5 |
+
reconcileCompletionTokens,
|
| 6 |
+
reconcileTotalTokens,
|
| 7 |
+
reportsModelWeightProgress,
|
| 8 |
+
requiresModelReload,
|
| 9 |
+
} from './App';
|
| 10 |
|
| 11 |
function engineError(code: string): EngineClientError {
|
| 12 |
return new EngineClientError({ code, message: code });
|
|
|
|
| 32 |
});
|
| 33 |
});
|
| 34 |
|
| 35 |
+
describe('App model-load progress', () => {
|
| 36 |
+
it('does not turn pinned manifest metadata reads into model-weight download state', () => {
|
| 37 |
+
expect(reportsModelWeightProgress('manifest')).toBe(false);
|
| 38 |
+
expect(reportsModelWeightProgress('download')).toBe(true);
|
| 39 |
+
expect(reportsModelWeightProgress('load')).toBe(true);
|
| 40 |
+
});
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
describe('App streamed completion telemetry', () => {
|
| 44 |
it('uses observed stream events when final engine usage is incomplete', () => {
|
| 45 |
expect(reconcileCompletionTokens(5, 64)).toBe(64);
|
src/app/App.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
| 8 |
type EngineCapabilities,
|
| 9 |
type EngineChatMessage,
|
| 10 |
type EngineChatToolCall,
|
|
|
|
| 11 |
type ManifestModelV2,
|
| 12 |
type ModelManifestV2,
|
| 13 |
type ModelTierId,
|
|
@@ -149,6 +150,12 @@ export function requiresModelReload(error: unknown): error is EngineClientError
|
|
| 149 |
].includes(error.code);
|
| 150 |
}
|
| 151 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
export function reconcileCompletionTokens(
|
| 153 |
engineCompletionTokens: number | undefined,
|
| 154 |
streamedTokenEvents: number,
|
|
@@ -475,6 +482,10 @@ export function App() {
|
|
| 475 |
}, {
|
| 476 |
signal: controller.signal,
|
| 477 |
onProgress: (progress) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
const received = Math.min(progress.loadedBytes, model.downloadBytes);
|
| 479 |
const shard = progress.shardIndex === null ? '' : ` · shard ${progress.shardIndex + 1}/${progress.shardCount}`;
|
| 480 |
setBootStatus(`${progress.phase}${shard} · ${formatBytes(received)} / ${formatBytes(model.downloadBytes)}`);
|
|
|
|
| 8 |
type EngineCapabilities,
|
| 9 |
type EngineChatMessage,
|
| 10 |
type EngineChatToolCall,
|
| 11 |
+
type EngineEvent,
|
| 12 |
type ManifestModelV2,
|
| 13 |
type ModelManifestV2,
|
| 14 |
type ModelTierId,
|
|
|
|
| 150 |
].includes(error.code);
|
| 151 |
}
|
| 152 |
|
| 153 |
+
export function reportsModelWeightProgress(
|
| 154 |
+
phase: Extract<EngineEvent, { event: 'progress' }>['phase'],
|
| 155 |
+
): boolean {
|
| 156 |
+
return phase !== 'manifest';
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
export function reconcileCompletionTokens(
|
| 160 |
engineCompletionTokens: number | undefined,
|
| 161 |
streamedTokenEvents: number,
|
|
|
|
| 482 |
}, {
|
| 483 |
signal: controller.signal,
|
| 484 |
onProgress: (progress) => {
|
| 485 |
+
if (!reportsModelWeightProgress(progress.phase)) {
|
| 486 |
+
setBootStatus('Reading pinned model manifest…');
|
| 487 |
+
return;
|
| 488 |
+
}
|
| 489 |
const received = Math.min(progress.loadedBytes, model.downloadBytes);
|
| 490 |
const shard = progress.shardIndex === null ? '' : ` · shard ${progress.shardIndex + 1}/${progress.shardCount}`;
|
| 491 |
setBootStatus(`${progress.phase}${shard} · ${formatBytes(received)} / ${formatBytes(model.downloadBytes)}`);
|