feat: implemented the v1 browser embedded LLM support (Gemma 4 E4B based)

This commit is contained in:
Xiaohan-Tian
2026-05-14 18:55:23 -07:00
parent 0e6e2736d0
commit 98c2b97413
30 changed files with 19462 additions and 153 deletions
@@ -1,6 +1,7 @@
import { KGConfigStorage } from '../io/KGConfigStorage';
import { CONFIG_UPGRADER_CONSTANTS } from '../../constants/coreConstants';
import { upgradeConfigToV1 } from './upgradeConfigToV1';
import { upgradeConfigToV2 } from './upgradeConfigToV2';
/**
* KGConfigUpgrader — Orchestrates app-level migrations (e.g., storage backend changes).
@@ -33,6 +34,10 @@ export class KGConfigUpgrader {
await upgradeConfigToV1();
break;
}
case 2: {
await upgradeConfigToV2();
break;
}
default: {
throw new Error(`No config upgrader found for version ${nextVersion}`);
}
@@ -0,0 +1,54 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const configStore = new Map<string, { name: string; data: Record<string, unknown>; lastModified: number }>();
vi.mock('../io/KGConfigStorage', () => ({
KGConfigStorage: {
getInstance: () => ({
getRaw: vi.fn(async (name: string) => configStore.get(name)?.data ?? null),
saveRaw: vi.fn(async (name: string, data: Record<string, unknown>) => {
configStore.set(name, { name, data, lastModified: Date.now() });
}),
}),
},
}));
import { upgradeConfigToV2 } from './upgradeConfigToV2';
describe('upgradeConfigToV2', () => {
beforeEach(() => {
configStore.clear();
});
it('pins legacy installs without an explicit provider to the old default provider', async () => {
configStore.set('userConfig', {
name: 'userConfig',
data: {
general: {
openai: { api_key: '', model: 'gpt-5.4-mini', flex: false },
},
},
lastModified: Date.now(),
});
await upgradeConfigToV2();
expect((configStore.get('userConfig')?.data.general as Record<string, unknown>).llm_provider).toBe('openai');
});
it('leaves explicit providers unchanged', async () => {
configStore.set('userConfig', {
name: 'userConfig',
data: {
general: {
llm_provider: 'openai_compatible',
},
},
lastModified: Date.now(),
});
await upgradeConfigToV2();
expect((configStore.get('userConfig')?.data.general as Record<string, unknown>).llm_provider).toBe('openai_compatible');
});
});
@@ -0,0 +1,25 @@
import { KGConfigStorage } from '../io/KGConfigStorage';
const CONFIG_KEY = 'userConfig';
const LEGACY_DEFAULT_PROVIDER = 'openai';
export async function upgradeConfigToV2(): Promise<void> {
const storage = KGConfigStorage.getInstance();
const rawConfig = await storage.getRaw(CONFIG_KEY);
if (!rawConfig || typeof rawConfig !== 'object') {
return;
}
const config = rawConfig as Record<string, unknown>;
const general = config.general;
if (!general || typeof general !== 'object') {
return;
}
if ('llm_provider' in (general as Record<string, unknown>)) {
return;
}
(general as Record<string, unknown>).llm_provider = LEGACY_DEFAULT_PROVIDER;
await storage.saveRaw(CONFIG_KEY, config);
}
+2 -2
View File
@@ -6,7 +6,7 @@ import { KGConfigStorage } from '../io/KGConfigStorage';
interface AppConfig {
general: {
language: string;
llm_provider: 'openai' | 'gemini' | 'claude' | 'claude_openrouter' | 'openai_compatible';
llm_provider: 'local_browser' | 'openai' | 'gemini' | 'claude' | 'claude_openrouter' | 'openai_compatible';
persist_api_keys_non_localhost: boolean;
openai: {
api_key: string;
@@ -183,7 +183,7 @@ export class ConfigManager {
this.defaultConfig = {
general: {
language: 'en_us',
llm_provider: 'openai',
llm_provider: 'local_browser',
persist_api_keys_non_localhost: false,
openai: {
api_key: '',
+8 -4
View File
@@ -9,10 +9,12 @@ class MockWritableFileStream {
this.handle = handle;
}
async write(content: ArrayBuffer | ArrayBufferView): Promise<void> {
const bytes = content instanceof ArrayBuffer
? new Uint8Array(content)
: new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
async write(content: ArrayBuffer | ArrayBufferView | string): Promise<void> {
const bytes = typeof content === 'string'
? new TextEncoder().encode(content)
: content instanceof ArrayBuffer
? new Uint8Array(content)
: new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
this.chunks.push(new Uint8Array(bytes));
}
@@ -44,6 +46,8 @@ class MockFileSystemFileHandle {
async getFile(): Promise<File> {
return {
size: this.content.byteLength,
text: async () => new TextDecoder().decode(this.content),
arrayBuffer: async () => this.content.buffer.slice(0),
} as unknown as File;
}