Add project files

This commit is contained in:
Kunthawat Greethong
2025-12-05 09:26:53 +07:00
parent 3b43cb52ef
commit 11986a0196
814 changed files with 141076 additions and 1 deletions

View File

@@ -0,0 +1,92 @@
# Fake LLM Server
A simple server that mimics the OpenAI streaming chat completions API for testing purposes.
## Features
- Implements a basic version of the OpenAI chat completions API
- Supports both streaming and non-streaming responses
- Always responds with "hello world" message
- Configurable through environment variables
## Installation
```bash
npm install
```
## Usage
Start the server:
```bash
# Development mode
npm run dev
# Production mode
npm run build
npm start
```
### Example usage
```
curl -X POST http://localhost:3500/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Say something"}],"model":"any-model","stream":true}'
```
The server will be available at http://localhost:3500 by default.
## API Endpoints
### POST /v1/chat/completions
This endpoint mimics OpenAI's chat completions API.
#### Request Format
```json
{
"messages": [{ "role": "user", "content": "Your prompt here" }],
"model": "any-model",
"stream": true
}
```
- Set `stream: true` to receive a streaming response
- Set `stream: false` or omit it for a regular JSON response
#### Response
For non-streaming requests, you'll get a standard JSON response:
```json
{
"id": "chatcmpl-123456789",
"object": "chat.completion",
"created": 1699000000,
"model": "fake-model",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "hello world"
},
"finish_reason": "stop"
}
]
}
```
For streaming requests, you'll receive a series of server-sent events (SSE), each containing a chunk of the response.
## Configuration
You can configure the server by modifying the `PORT` variable in the code.
## Use Case
This server is primarily intended for testing applications that integrate with OpenAI's API, allowing you to develop and test without making actual API calls to OpenAI.

View File

@@ -0,0 +1,255 @@
import { Request, Response } from "express";
import fs from "fs";
import path from "path";
import { CANNED_MESSAGE, createStreamChunk } from ".";
let globalCounter = 0;
export const createChatCompletionHandler =
(prefix: string) => (req: Request, res: Response) => {
const { stream = false, messages = [] } = req.body;
console.log("* Received messages", messages);
// Disabled rate limit simulation
const lastMessage = messages[messages.length - 1];
let messageContent = CANNED_MESSAGE;
if (
lastMessage &&
Array.isArray(lastMessage.content) &&
lastMessage.content.some(
(part: { type: string; text: string }) =>
part.type === "text" &&
part.text.includes("[[UPLOAD_IMAGE_TO_CODEBASE]]"),
)
) {
messageContent = `Uploading image to codebase
<dyad-write path="new/image/file.png" description="Uploaded image to codebase">
DYAD_ATTACHMENT_0
</dyad-write>
`;
messageContent += "\n\n" + generateDump(req);
}
// TS auto-fix prefixes
if (
lastMessage &&
typeof lastMessage.content === "string" &&
lastMessage.content.startsWith(
"Fix these 2 TypeScript compile-time error",
)
) {
// Fix errors in create-ts-errors.md and introduce a new error
messageContent = `
<dyad-write path="src/bad-file.ts" description="Fix 2 errors and introduce a new error.">
// Import doesn't exist
// import NonExistentClass from 'non-existent-class';
const x = new Object();
x.nonExistentMethod2();
</dyad-write>
`;
}
if (
lastMessage &&
typeof lastMessage.content === "string" &&
lastMessage.content.startsWith(
"Fix these 1 TypeScript compile-time error",
)
) {
// Fix errors in create-ts-errors.md and introduce a new error
messageContent = `
<dyad-write path="src/bad-file.ts" description="Fix remaining error.">
// Import doesn't exist
// import NonExistentClass from 'non-existent-class';
const x = new Object();
x.toString(); // replaced with existing method
</dyad-write>
`;
}
if (
lastMessage &&
typeof lastMessage.content === "string" &&
lastMessage.content.includes("TypeScript compile-time error")
) {
messageContent += "\n\n" + generateDump(req);
}
if (
lastMessage &&
typeof lastMessage.content === "string" &&
lastMessage.content.startsWith("Fix error: Error Line 6 error")
) {
messageContent = `
Fixing the error...
<dyad-write path="src/pages/Index.tsx">
import { MadeWithDyad } from "@/components/made-with-dyad";
const Index = () => {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4">No more errors!</h1>
</div>
<MadeWithDyad />
</div>
);
};
export default Index;
</dyad-write>
`;
}
console.error("LASTMESSAGE", lastMessage);
// Check if the last message is "[dump]" to write messages to file and return path
if (
lastMessage &&
(Array.isArray(lastMessage.content)
? lastMessage.content.some(
(part: { type: string; text: string }) =>
part.type === "text" && part.text.includes("[dump]"),
)
: lastMessage.content.includes("[dump]"))
) {
messageContent = generateDump(req);
}
if (lastMessage && lastMessage.content === "[increment]") {
globalCounter++;
messageContent = `counter=${globalCounter}`;
}
// Check if the last message starts with "tc=" to load test case file
if (
lastMessage &&
lastMessage.content &&
typeof lastMessage.content === "string" &&
lastMessage.content.startsWith("tc=")
) {
const testCaseName = lastMessage.content.slice(3); // Remove "tc=" prefix
const testFilePath = path.join(
__dirname,
"..",
"..",
"..",
"e2e-tests",
"fixtures",
prefix,
`${testCaseName}.md`,
);
try {
if (fs.existsSync(testFilePath)) {
messageContent = fs.readFileSync(testFilePath, "utf-8");
console.log(`* Loaded test case: ${testCaseName}`);
} else {
console.log(`* Test case file not found: ${testFilePath}`);
messageContent = `Error: Test case file not found: ${testCaseName}.md`;
}
} catch (error) {
console.error(`* Error reading test case file: ${error}`);
messageContent = `Error: Could not read test case file: ${testCaseName}.md`;
}
}
if (
lastMessage &&
lastMessage.content &&
typeof lastMessage.content === "string" &&
lastMessage.content.trim().endsWith("[[STRING_TO_BE_FINISHED]]")
) {
messageContent = `[[STRING_IS_FINISHED]]";</dyad-write>\nFinished writing file.`;
messageContent += "\n\n" + generateDump(req);
}
// Non-streaming response
if (!stream) {
return res.json({
id: `chatcmpl-${Date.now()}`,
object: "chat.completion",
created: Math.floor(Date.now() / 1000),
model: "fake-model",
choices: [
{
index: 0,
message: {
role: "assistant",
content: messageContent,
},
finish_reason: "stop",
},
],
});
}
// Streaming response
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
// Split the message into characters to simulate streaming
const message = messageContent;
const messageChars = message.split("");
// Stream each character with a delay
let index = 0;
const batchSize = 8;
// Send role first
res.write(createStreamChunk("", "assistant"));
const interval = setInterval(() => {
if (index < messageChars.length) {
// Get the next batch of characters (up to batchSize)
const batch = messageChars.slice(index, index + batchSize).join("");
res.write(createStreamChunk(batch));
index += batchSize;
} else {
// Send the final chunk
res.write(createStreamChunk("", "assistant", true));
clearInterval(interval);
res.end();
}
}, 10);
};
function generateDump(req: Request) {
const timestamp = Date.now();
const generatedDir = path.join(__dirname, "generated");
// Create generated directory if it doesn't exist
if (!fs.existsSync(generatedDir)) {
fs.mkdirSync(generatedDir, { recursive: true });
}
const dumpFilePath = path.join(generatedDir, `${timestamp}.json`);
try {
fs.writeFileSync(
dumpFilePath,
JSON.stringify(
{
body: req.body,
headers: { authorization: req.headers["authorization"] },
},
null,
2,
).replace(/\r\n/g, "\n"),
"utf-8",
);
console.log(`* Dumped messages to: ${dumpFilePath}`);
return `[[dyad-dump-path=${dumpFilePath}]]`;
} catch (error) {
console.error(`* Error writing dump file: ${error}`);
return `Error: Could not write dump file: ${error}`;
}
}

View File

@@ -0,0 +1,418 @@
import { Request, Response } from "express";
import * as path from "path";
import * as fs from "fs";
import * as os from "os";
const gitHttpMiddlewareFactory = require("git-http-mock-server/middleware");
// Push event tracking for tests
interface PushEvent {
timestamp: Date;
repo: string;
branch: string;
operation: "push" | "create" | "delete";
commitSha?: string;
}
const pushEvents: PushEvent[] = [];
// Mock data for testing
const mockAccessToken = "fake_access_token_12345";
const mockDeviceCode = "fake_device_code_12345";
const mockUserCode = "FAKE-CODE";
const mockUser = {
login: "testuser",
id: 12345,
email: "testuser@example.com",
};
const mockRepos = [
{
id: 1,
name: "test-repo-1",
full_name: "testuser/test-repo-1",
private: false,
owner: { login: "testuser" },
default_branch: "main",
},
{
id: 2,
name: "test-repo-2",
full_name: "testuser/test-repo-2",
private: true,
owner: { login: "testuser" },
default_branch: "main",
},
{
id: 3,
name: "existing-app",
full_name: "testuser/existing-app",
private: false,
owner: { login: "testuser" },
default_branch: "main",
},
];
const mockBranches = [
{ name: "main", commit: { sha: "abc123" } },
{ name: "develop", commit: { sha: "def456" } },
{ name: "feature/test", commit: { sha: "ghi789" } },
];
// Store device flow state
let deviceFlowState = {
deviceCode: mockDeviceCode,
userCode: mockUserCode,
authorized: false,
pollCount: 0,
};
// GitHub Device Flow - Step 1: Get device code
export function handleDeviceCode(req: Request, res: Response) {
console.log("* GitHub Device Code requested");
// Reset state for new flow
deviceFlowState = {
deviceCode: mockDeviceCode,
userCode: mockUserCode,
authorized: false,
pollCount: 0,
};
res.json({
device_code: mockDeviceCode,
user_code: mockUserCode,
verification_uri: "https://github.com/login/device",
verification_uri_complete: `https://github.com/login/device?user_code=${mockUserCode}`,
expires_in: 900,
interval: 1, // Short interval for testing
});
}
// GitHub Device Flow - Step 2: Poll for access token
export function handleAccessToken(req: Request, res: Response) {
console.log("* GitHub Access Token polling", {
pollCount: deviceFlowState.pollCount,
});
const { device_code } = req.body;
if (device_code !== mockDeviceCode) {
return res.status(400).json({
error: "invalid_request",
error_description: "Invalid device code",
});
}
deviceFlowState.pollCount++;
// Simulate authorization after 3 polls (for testing)
if (deviceFlowState.pollCount >= 3) {
deviceFlowState.authorized = true;
return res.json({
access_token: mockAccessToken,
token_type: "bearer",
scope: "repo,user,workflow",
});
}
// Return pending status
res.status(400).json({
error: "authorization_pending",
error_description: "The authorization request is still pending",
});
}
// Get authenticated user info
export function handleUser(req: Request, res: Response) {
console.log("* GitHub User info requested");
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.includes(mockAccessToken)) {
return res.status(401).json({
message: "Bad credentials",
});
}
res.json(mockUser);
}
// Get user emails
export function handleUserEmails(req: Request, res: Response) {
console.log("* GitHub User emails requested");
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.includes(mockAccessToken)) {
return res.status(401).json({
message: "Bad credentials",
});
}
res.json([
{
email: "testuser@example.com",
primary: true,
verified: true,
visibility: "public",
},
]);
}
// List user repositories
export function handleUserRepos(req: Request, res: Response) {
console.log("* GitHub User repos requested");
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.includes(mockAccessToken)) {
return res.status(401).json({
message: "Bad credentials",
});
}
if (req.method === "GET") {
// List repos
res.json(mockRepos);
} else if (req.method === "POST") {
// Create repo
const { name, private: isPrivate } = req.body;
console.log("* Creating repository:", name);
// Check if repo already exists
const existingRepo = mockRepos.find((repo) => repo.name === name);
if (existingRepo) {
return res.status(422).json({
message: "Repository creation failed.",
errors: [
{
resource: "Repository",
code: "already_exists",
field: "name",
},
],
});
}
// Create new repo
const newRepo = {
id: mockRepos.length + 1,
name,
full_name: `${mockUser.login}/${name}`,
private: !!isPrivate,
owner: { login: mockUser.login },
default_branch: "main",
};
res.status(201).json(newRepo);
}
}
// Get repository info
export function handleRepo(req: Request, res: Response) {
console.log("* GitHub Repo info requested");
const { owner, repo } = req.params;
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.includes(mockAccessToken)) {
return res.status(401).json({
message: "Bad credentials",
});
}
const foundRepo = mockRepos.find((r) => r.full_name === `${owner}/${repo}`);
if (!foundRepo) {
return res.status(404).json({
message: "Not Found",
});
}
res.json(foundRepo);
}
// Get repository branches
export function handleRepoBranches(req: Request, res: Response) {
console.log("* GitHub Repo branches requested");
const { owner, repo } = req.params;
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.includes(mockAccessToken)) {
return res.status(401).json({
message: "Bad credentials",
});
}
const foundRepo = mockRepos.find((r) => r.full_name === `${owner}/${repo}`);
if (!foundRepo) {
return res.status(404).json({
message: "Not Found",
});
}
res.json(mockBranches);
}
// Create repository for organization (not implemented in mock)
export function handleOrgRepos(req: Request, res: Response) {
console.log("* GitHub Org repos requested");
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.includes(mockAccessToken)) {
return res.status(401).json({
message: "Bad credentials",
});
}
// For simplicity, just redirect to user repos for mock
handleUserRepos(req, res);
}
// Push event management functions for testing
export function handleGetPushEvents(req: Request, res: Response) {
console.log("* Getting push events");
const { repo } = req.query;
const events = repo ? pushEvents.filter((e) => e.repo === repo) : pushEvents;
res.json(events);
}
export function handleClearPushEvents(req: Request, res: Response) {
console.log("* Clearing push events");
pushEvents.length = 0;
res.json({ cleared: true, timestamp: new Date() });
}
// Handle Git operations (push, pull, clone, etc.) using git-http-mock-server
export function handleGitPush(req: Request, res: Response, next?: Function) {
console.log("* GitHub Git operation requested:", req.method, req.url);
// Log request headers to see git operation details
console.log("* Git Headers:", {
"git-protocol": req.headers["git-protocol"],
"content-type": req.headers["content-type"],
"user-agent": req.headers["user-agent"],
});
// Create a unique temporary directory for this request
const mockReposRoot = fs.mkdtempSync(
path.join(
os.tmpdir(),
"dyad-git-mock-" + Math.random().toString(36).substring(2, 15),
),
);
console.error(`* Created temporary git repos directory: ${mockReposRoot}`);
// Create git middleware instance for this request
const gitHttpMiddleware = gitHttpMiddlewareFactory({
root: mockReposRoot,
route: "/github/git",
glob: "*.git",
});
// Extract repo name from URL path like /github/git/testuser/test-repo.git
// The middleware expects the repo name as the basename after the route
const urlPath = req.url;
const match = urlPath.match(/\/github\/git\/[^/]+\/([^/.]+)\.git/);
const repoName = match?.[1];
if (repoName) {
console.log(`* Git operation for repo: ${repoName}`);
// Track push events if this is a git-receive-pack (push) operation
if (req.url.includes("/git-receive-pack") && req.method === "POST") {
console.log("* Git PUSH operation detected for repo:", repoName);
// Collect request body to parse git protocol
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", () => {
try {
// Parse git pack protocol for branch refs
// Git protocol sends refs in format: "old-sha new-sha refs/heads/branch-name"
const lines = body.split("\n");
lines.forEach((line) => {
// Look for lines containing refs/heads/
const refMatch = line.match(
// eslint-disable-next-line
/([0-9a-f]{40})\s+([0-9a-f]{40})\s+refs\/heads\/([^\s\u0000]+)/,
);
if (refMatch) {
const [, oldSha, newSha, branchName] = refMatch;
const isDelete = newSha === "0".repeat(40);
const isCreate = oldSha === "0".repeat(40);
let operation: "push" | "create" | "delete" = "push";
if (isDelete) operation = "delete";
else if (isCreate) operation = "create";
pushEvents.push({
timestamp: new Date(),
repo: repoName,
branch: branchName,
operation,
commitSha: isDelete ? oldSha : newSha,
});
console.log(
`* Recorded ${operation} to ${repoName}/${branchName}, commit: ${isDelete ? oldSha : newSha}`,
);
}
});
} catch (error) {
console.error("* Error parsing git protocol:", error);
}
});
}
// Ensure the bare git repository exists for this repo
const bareRepoPath = path.join(mockReposRoot, `${repoName}.git`);
console.log(`* Creating bare git repository at: ${bareRepoPath}`);
try {
fs.mkdirSync(bareRepoPath, { recursive: true });
// Initialize as bare repository
const { execSync } = require("child_process");
execSync(`git init --bare`, { cwd: bareRepoPath });
console.log(
`* Successfully created bare git repository: ${repoName}.git`,
);
} catch (error) {
console.error(`* Failed to create bare git repository:`, error);
return res.status(500).json({
message: "Failed to initialize git repository",
error: error instanceof Error ? error.message : String(error),
});
}
// Rewrite the URL to match what the middleware expects
// Change /github/git/testuser/test-repo.git/... to /github/git/test-repo.git/...
const rewrittenUrl = req.url.replace(
/\/github\/git\/[^/]+\//,
"/github/git/",
);
req.url = rewrittenUrl;
console.log(`* Rewritten URL from ${urlPath} to ${rewrittenUrl}`);
}
// Use git-http-mock-server middleware to handle the actual git operations
gitHttpMiddleware(
req,
res,
next ||
(() => {
// Fallback if middleware doesn't handle the request
console.log(
`* Git middleware did not handle request: ${req.method} ${req.url}`,
);
res.status(404).json({
message: "Git operation not supported",
url: req.url,
method: req.method,
});
}),
);
}

View File

@@ -0,0 +1,186 @@
import express from "express";
import { createServer } from "http";
import cors from "cors";
import { createChatCompletionHandler } from "./chatCompletionHandler";
import {
handleDeviceCode,
handleAccessToken,
handleUser,
handleUserEmails,
handleUserRepos,
handleRepo,
handleRepoBranches,
handleOrgRepos,
handleGitPush,
handleGetPushEvents,
handleClearPushEvents,
} from "./githubHandler";
// Create Express app
const app = express();
app.use(cors());
app.use(express.json({ limit: "50mb" }));
app.use(express.urlencoded({ extended: true, limit: "50mb" }));
const PORT = 3500;
// Helper function to create OpenAI-like streaming response chunks
export function createStreamChunk(
content: string,
role: string = "assistant",
isLast: boolean = false,
) {
const chunk = {
id: `chatcmpl-${Date.now()}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: "fake-model",
choices: [
{
index: 0,
delta: isLast ? {} : { content, role },
finish_reason: isLast ? "stop" : null,
},
],
};
return `data: ${JSON.stringify(chunk)}\n\n${isLast ? "data: [DONE]\n\n" : ""}`;
}
export const CANNED_MESSAGE = `
<dyad-write path="file1.txt">
A file (2)
</dyad-write>
More
EOM`;
app.get("/health", (req, res) => {
res.send("OK");
});
// Ollama-specific endpoints
app.get("/ollama/api/tags", (req, res) => {
const ollamaModels = {
models: [
{
name: "testollama",
modified_at: "2024-05-01T10:00:00.000Z",
size: 4700000000,
digest: "abcdef123456",
details: {
format: "gguf",
family: "llama",
families: ["llama"],
parameter_size: "8B",
quantization_level: "Q4_0",
},
},
{
name: "codellama:7b",
modified_at: "2024-04-25T12:30:00.000Z",
size: 3800000000,
digest: "fedcba654321",
details: {
format: "gguf",
family: "llama",
families: ["llama", "codellama"],
parameter_size: "7B",
quantization_level: "Q5_K_M",
},
},
],
};
console.log("* Sending fake Ollama models");
res.json(ollamaModels);
});
// LM Studio specific endpoints
app.get("/lmstudio/api/v0/models", (req, res) => {
const lmStudioModels = {
data: [
{
type: "llm",
id: "lmstudio-model-1",
object: "model",
publisher: "lmstudio",
state: "loaded",
max_context_length: 4096,
quantization: "Q4_0",
compatibility_type: "gguf",
arch: "llama",
},
{
type: "llm",
id: "lmstudio-model-2-chat",
object: "model",
publisher: "lmstudio",
state: "not-loaded",
max_context_length: 8192,
quantization: "Q5_K_M",
compatibility_type: "gguf",
arch: "mixtral",
},
{
type: "embedding", // Should be filtered out by client
id: "lmstudio-embedding-model",
object: "model",
publisher: "lmstudio",
state: "loaded",
max_context_length: 2048,
quantization: "F16",
compatibility_type: "gguf",
arch: "bert",
},
],
};
console.log("* Sending fake LM Studio models");
res.json(lmStudioModels);
});
["lmstudio", "gateway", "engine", "ollama"].forEach((provider) => {
app.post(
`/${provider}/v1/chat/completions`,
createChatCompletionHandler(provider),
);
});
// Default test provider handler:
app.post("/v1/chat/completions", createChatCompletionHandler("."));
// GitHub API Mock Endpoints
console.log("Setting up GitHub mock endpoints");
// GitHub OAuth Device Flow
app.post("/github/login/device/code", handleDeviceCode);
app.post("/github/login/oauth/access_token", handleAccessToken);
// GitHub API endpoints
app.get("/github/api/user", handleUser);
app.get("/github/api/user/emails", handleUserEmails);
app.get("/github/api/user/repos", handleUserRepos);
app.post("/github/api/user/repos", handleUserRepos);
app.get("/github/api/repos/:owner/:repo", handleRepo);
app.get("/github/api/repos/:owner/:repo/branches", handleRepoBranches);
app.post("/github/api/orgs/:org/repos", handleOrgRepos);
// GitHub test endpoints for verifying push operations
app.get("/github/api/test/push-events", handleGetPushEvents);
app.post("/github/api/test/clear-push-events", handleClearPushEvents);
// GitHub Git endpoints - intercept all paths with /github/git prefix
app.all("/github/git/*", handleGitPush);
// Start the server
const server = createServer(app);
server.listen(PORT, () => {
console.log(`Fake LLM server running on http://localhost:${PORT}`);
});
// Handle SIGINT (Ctrl+C)
process.on("SIGINT", () => {
console.log("Shutting down fake LLM server");
server.close(() => {
console.log("Server closed");
process.exit(0);
});
});

1628
testing/fake-llm-server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
{
"name": "fake-llm-server",
"version": "1.0.0",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node index.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "Fake OpenAI API server for testing",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2",
"stream": "0.0.2"
},
"devDependencies": {
"@types/cors": "^2.8.18",
"@types/express": "^4.17.21",
"@types/node": "^20.17.46",
"git-http-mock-server": "^2.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
}
}

View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["*.ts"],
"exclude": ["node_modules"]
}