WaveCut commited on
Commit
20134b6
·
verified ·
1 Parent(s): 3fd9ca4

Release v1.1.4 manifest progress fix

Browse files

Correct manifest progress accounting and document verified Safari and Firefox gates; Windows hardware gates remain open.

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. Edge/Windows, Safari, and Firefox remain explicit release-matrix gaps;
62
- dense CPU fallback is offered only when that browser exposes the required
63
- cross-origin-isolated WASM features.
 
 
 
 
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-DIzN0g8r.js"></script>
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 { DEFAULT_MODEL_ID, reconcileCompletionTokens, reconcileTotalTokens, requiresModelReload } from './App';
 
 
 
 
 
 
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)}`);