Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
231efa0
feat: Add save/cancel buttons to settings panel
Feb 1, 2026
a8e853b
fix: optimize Tailwind content config and add Mermaid dynamic loading
Feb 2, 2026
2b9cd73
merge: sync with upstream (yeahhe365/All-Model-Chat)
Feb 2, 2026
77fac62
WIP: Agentic folder access - function call loop (needs debugging)
Feb 2, 2026
75d86e8
fix: handle empty parts in function call continuation
Feb 5, 2026
e5b8839
Fix thought signature for tool calls
Feb 5, 2026
c9d736c
Unify folder/zip import handling
Feb 5, 2026
5bf62fc
Ignore agent and chat folders
Feb 5, 2026
2624552
Merge upstream changes with local features
Feb 5, 2026
6782d21
Improve streaming tool handling and auto-continue
Feb 5, 2026
e0fb5e6
ignore .agent and chat
Feb 5, 2026
2a58f7c
remove top-level .agent
Feb 5, 2026
9f404bd
remove root package-lock
Feb 5, 2026
1eb9ec2
Remove package-lock
Feb 5, 2026
a853d19
fix: preserve chat/session persistence across restarts
Feb 7, 2026
6f722f6
chore: extend ignore rules for generated and data files
Feb 12, 2026
9c78d14
chore(monorepo): initialize workspace layout
Feb 12, 2026
c02f8f6
refactor(web): migrate frontend source into apps/web workspace
Feb 12, 2026
3e45bf2
feat(bff): bootstrap service with health endpoint
Feb 12, 2026
72b40fc
feat(bff): add provider key pool and gemini client wrapper
Feb 12, 2026
f90a3df
feat(bff): implement /api/chat/stream proxy
Feb 12, 2026
0d88d29
feat(bff): add explicit provider endpoint config
Feb 12, 2026
cbda083
refactor(web): switch chat stream adapter to bff
Feb 12, 2026
beea662
feat(bff+web): migrate generation and file endpoints
Feb 12, 2026
ab55ef6
security(web): remove frontend key persistence
Feb 12, 2026
cfbe245
security(web): remove raw-key analytics and add migration cleanup
Feb 12, 2026
7cf67d5
build(web): unify dependency source and runtime versions
Feb 12, 2026
b4735a3
refactor(shared): extract shared contracts to packages
Feb 12, 2026
f52482c
chore(quality): add lint/typecheck/test/build gates + CI
Feb 12, 2026
ca238eb
chore(lockfile): add workspace package-lock
Feb 12, 2026
40e91b4
chore(gitignore): ignore local setup directory
Feb 12, 2026
133405c
feat(web): switch api config source between custom and backend default
Feb 12, 2026
5810a88
fix(api): enforce custom key usage without backend fallback
Feb 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
29 changes: 29 additions & 0 deletions all-model-chat/.github/workflows/quality-gates.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Quality Gates

on:
push:
branches:
- '**'
pull_request:

permissions:
contents: read

jobs:
quality-gates:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm install

- name: Run quality checks
run: npm run ci:check
23 changes: 23 additions & 0 deletions all-model-chat/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,33 @@ dist-ssr
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.github/*
!.github/workflows/
!.github/workflows/**
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

# Generated files
bundle-stats.html
stats.html
*.png
*.pdf
*.csv
*.pkl
__pycache__/

# Temporary folders
/chat-answer/
/.agent/
/chat/
/setup/

# Environment files
.env
.env.local
.env.*.local
28 changes: 28 additions & 0 deletions all-model-chat/apps/bff/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# BFF runtime configuration
BFF_HOST=127.0.0.1
BFF_PORT=8787
BFF_SERVICE_NAME=all-model-chat-bff
NODE_ENV=development

# Provider key configuration
# Supports comma/newline separated key lists.
GEMINI_API_KEYS=your-first-key,your-second-key

# Optional single key fallback
# GEMINI_API_KEY=your-single-key

# Cooldown duration (milliseconds) after a key-level failure
BFF_KEY_FAILURE_COOLDOWN_MS=30000

# Provider endpoint mode
# false => Gemini API
# true => Vertex AI / Vertex Express
BFF_PROVIDER_VERTEXAI=false

# Optional provider endpoint override (full base URL without trailing slash)
# Example for Vertex Express: https://aiplatform.googleapis.com
# BFF_PROVIDER_BASE_URL=https://aiplatform.googleapis.com

# Optional API version override
# Example for Vertex Express stable: v1
# BFF_PROVIDER_API_VERSION=v1
18 changes: 18 additions & 0 deletions all-model-chat/apps/bff/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "@all-model-chat/bff",
"private": true,
"version": "1.8.5",
"type": "module",
"main": "dist/index.js",
"scripts": {
"dev": "npm run build && npm run start",
"build": "tsc -p tsconfig.json",
"start": "node dist/index.js",
"lint": "tsc -p tsconfig.json --noEmit",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "node --test"
},
"dependencies": {
"@google/genai": "^1.2.0"
}
}
81 changes: 81 additions & 0 deletions all-model-chat/apps/bff/src/config/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
export interface BffConfig {
host: string;
port: number;
nodeEnv: string;
serviceName: string;
providerApiKeys: string[];
providerKeyFailureCooldownMs: number;
providerUseVertexAi: boolean;
providerBaseUrl?: string;
providerApiVersion?: string;
}

const parsePort = (rawPort: string | undefined, fallback: number): number => {
if (!rawPort) return fallback;

const parsed = Number(rawPort);
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
throw new Error(`Invalid BFF_PORT value: ${rawPort}`);
}

return parsed;
};

const parsePositiveInteger = (rawValue: string | undefined, fallback: number, fieldName: string): number => {
if (!rawValue) return fallback;

const parsed = Number(rawValue);
if (!Number.isInteger(parsed) || parsed <= 0) {
throw new Error(`Invalid ${fieldName} value: ${rawValue}`);
}

return parsed;
};

const parseBoolean = (rawValue: string | undefined, fallback: boolean, fieldName: string): boolean => {
if (!rawValue) return fallback;

const normalized = rawValue.trim().toLowerCase();
if (normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on') {
return true;
}
if (normalized === 'false' || normalized === '0' || normalized === 'no' || normalized === 'off') {
return false;
}

throw new Error(`Invalid ${fieldName} value: ${rawValue}`);
};

const parseOptionalString = (rawValue: string | undefined): string | undefined => {
if (!rawValue) return undefined;

const trimmed = rawValue.trim();
return trimmed.length > 0 ? trimmed : undefined;
};

const parseProviderApiKeys = (rawList: string | undefined, rawSingle: string | undefined): string[] => {
const merged = [rawList || '', rawSingle || ''].join('\n');

return merged
.split(/[\n,]+/)
.map((key) => key.trim())
.filter((key) => key.length > 0);
};

export const loadBffConfig = (): BffConfig => {
return {
host: process.env.BFF_HOST || '127.0.0.1',
port: parsePort(process.env.BFF_PORT, 8787),
nodeEnv: process.env.NODE_ENV || 'development',
serviceName: process.env.BFF_SERVICE_NAME || 'all-model-chat-bff',
providerApiKeys: parseProviderApiKeys(process.env.GEMINI_API_KEYS, process.env.GEMINI_API_KEY),
providerKeyFailureCooldownMs: parsePositiveInteger(
process.env.BFF_KEY_FAILURE_COOLDOWN_MS,
30000,
'BFF_KEY_FAILURE_COOLDOWN_MS'
),
providerUseVertexAi: parseBoolean(process.env.BFF_PROVIDER_VERTEXAI, false, 'BFF_PROVIDER_VERTEXAI'),
providerBaseUrl: parseOptionalString(process.env.BFF_PROVIDER_BASE_URL),
providerApiVersion: parseOptionalString(process.env.BFF_PROVIDER_API_VERSION),
};
};
119 changes: 119 additions & 0 deletions all-model-chat/apps/bff/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { createServer } from 'node:http';
import { loadBffConfig } from './config/env.js';
import { createHealthPayload } from './routes/health.js';
import { ProviderKeyPool } from './providers/keyPool.js';
import { GeminiProviderClient } from './providers/geminiClient.js';
import { handleChatStreamRoute } from './routes/chatStream.js';
import { handleFilesRoute } from './routes/files.js';
import { handleGenerationRoute } from './routes/generation.js';

const config = loadBffConfig();
const keyPool = new ProviderKeyPool(config.providerApiKeys, {
failureCooldownMs: config.providerKeyFailureCooldownMs,
});
const geminiProviderClient = new GeminiProviderClient(keyPool, {
useVertexAi: config.providerUseVertexAi,
baseUrl: config.providerBaseUrl,
apiVersion: config.providerApiVersion,
});

const server = createServer((request, response) => {
if (!request.url) {
response.writeHead(400, { 'content-type': 'application/json; charset=utf-8' });
response.end(JSON.stringify({ error: 'Invalid request URL' }));
return;
}

const method = request.method || 'GET';
const path = request.url.split('?')[0];

if (method === 'GET' && path === '/health') {
const payload = createHealthPayload(
config,
geminiProviderClient.getKeyPoolSnapshot(),
geminiProviderClient.getProviderConfigSnapshot()
);
response.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
response.end(JSON.stringify(payload));
return;
}

if (path === '/api/chat/stream') {
if (method !== 'POST') {
response.writeHead(405, {
'content-type': 'application/json; charset=utf-8',
allow: 'POST',
});
response.end(JSON.stringify({ error: 'Method Not Allowed' }));
return;
}

handleChatStreamRoute(request, response, geminiProviderClient).catch((error) => {
if (response.writableEnded) {
return;
}

const message = error instanceof Error ? error.message : 'Unexpected stream proxy failure.';
response.writeHead(500, { 'content-type': 'application/json; charset=utf-8' });
response.end(
JSON.stringify({
error: {
code: 'internal_error',
message,
status: 500,
},
})
);
});
return;
}

if (path.startsWith('/api/files/')) {
handleFilesRoute(request, response, geminiProviderClient).catch((error) => {
if (response.writableEnded) return;
const message = error instanceof Error ? error.message : 'Unexpected files route failure.';
response.writeHead(500, { 'content-type': 'application/json; charset=utf-8' });
response.end(
JSON.stringify({
error: {
code: 'internal_error',
message,
status: 500,
},
})
);
});
return;
}

if (path.startsWith('/api/generation/')) {
handleGenerationRoute(request, response, geminiProviderClient).catch((error) => {
if (response.writableEnded) return;
const message = error instanceof Error ? error.message : 'Unexpected generation route failure.';
response.writeHead(500, { 'content-type': 'application/json; charset=utf-8' });
response.end(
JSON.stringify({
error: {
code: 'internal_error',
message,
status: 500,
},
})
);
});
return;
}

response.writeHead(404, { 'content-type': 'application/json; charset=utf-8' });
response.end(JSON.stringify({ error: 'Not Found' }));
});

server.listen(config.port, config.host, () => {
console.log(`[BFF] ${config.serviceName} listening on http://${config.host}:${config.port}`);
console.log(`[BFF] Provider key pool initialized with ${config.providerApiKeys.length} key(s).`);
console.log(
`[BFF] Provider mode: ${config.providerUseVertexAi ? 'vertexai' : 'gemini-api'} (baseUrl=${
config.providerBaseUrl || 'default'
}, apiVersion=${config.providerApiVersion || 'default'})`
);
});
Loading