adding home page

This commit is contained in:
2026-07-21 10:53:02 -05:00
parent 4e7e86a3be
commit 0e9131dafa
4 changed files with 130 additions and 1 deletions
+10
View File
@@ -0,0 +1,10 @@
# OpenCode Go Proxy
A small OpenAI-compatible and Anthropic-compatible proxy for OpenCode Go.
## Endpoints
- OpenAI-compatible API: `/oai/v1`
- Anthropic-compatible API: `/ant/v1`
Use the API with your preferred OpenAI or Anthropic-compatible client. See the [README](https://github.com/owenqwenstarsky/opencode-proxy) for setup and request examples.
+92
View File
@@ -0,0 +1,92 @@
function escapeHtml(value) {
return value
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
function inlineMarkdown(value) {
let html = escapeHtml(value);
html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2">$1</a>');
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
return html;
}
export function renderMarkdown(markdown) {
const lines = markdown.replaceAll('\r\n', '\n').split('\n');
const output = [];
let paragraph = [];
let list = [];
let code = null;
const flushParagraph = () => {
if (paragraph.length) {
output.push(`<p>${inlineMarkdown(paragraph.join(' '))}</p>`);
paragraph = [];
}
};
const flushList = () => {
if (list.length) {
output.push(`<ul>${list.map((item) => `<li>${inlineMarkdown(item)}</li>`).join('')}</ul>`);
list = [];
}
};
for (const line of lines) {
if (code) {
if (line.startsWith('```')) {
output.push(`<pre><code>${escapeHtml(code.join('\n'))}</code></pre>`);
code = null;
} else {
code.push(line);
}
continue;
}
if (line.startsWith('```')) {
flushParagraph();
flushList();
code = [];
} else if (/^#{1,6} /.test(line)) {
flushParagraph();
flushList();
const [, hashes, heading] = line.match(/^(#{1,6}) (.+)$/);
output.push(`<h${hashes.length}>${inlineMarkdown(heading)}</h${hashes.length}>`);
} else if (/^- /.test(line)) {
flushParagraph();
list.push(line.slice(2));
} else if (line.trim() === '') {
flushParagraph();
flushList();
} else {
flushList();
paragraph.push(line.trim());
}
}
if (code) output.push(`<pre><code>${escapeHtml(code.join('\n'))}</code></pre>`);
flushParagraph();
flushList();
return output.join('\n');
}
export function renderPage(markdown, { title = 'OpenCode Go Proxy' } = {}) {
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${escapeHtml(title)}</title>
<style>
:root { color-scheme: dark; font-family: system-ui, sans-serif; }
body { max-width: 50rem; margin: 0 auto; padding: 3rem 1.25rem; background: #101114; color: #f1f3f5; line-height: 1.6; }
a { color: #8ab4ff; } code { padding: .15rem .35rem; background: #252830; border-radius: .25rem; }
pre { overflow-x: auto; padding: 1rem; background: #191b21; border-radius: .5rem; }
</style>
</head>
<body>${renderMarkdown(markdown)}</body>
</html>`;
}
+15 -1
View File
@@ -1,5 +1,9 @@
import { Readable } from 'node:stream';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import express from 'express';
import { renderPage } from './markdown.js';
import { createKeyRotator } from './key-store.js';
import { clientIp, createRateLimiter, numericCost, rateLimitError } from './rate-limit.js';
import {
@@ -26,6 +30,7 @@ async function forwardError(upstream, response) {
}
export const DEFAULT_MAX_CONTEXT_TOKENS = 262144;
const defaultPagesDirectory = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'pages');
function maxContextFromEnv(value = process.env.MAX_CONTEXT_TOKENS) {
if (value === undefined) return DEFAULT_MAX_CONTEXT_TOKENS;
@@ -54,11 +59,20 @@ function withContextLimit(payload, maxContextTokens) {
};
}
export function createProxyApp({ keys, fetchImpl = globalThis.fetch, upstreamBaseUrl = DEFAULT_UPSTREAM_BASE_URL, maxContextTokens = maxContextFromEnv(), rateLimiter = createRateLimiter() } = {}) {
export function createProxyApp({ keys, fetchImpl = globalThis.fetch, upstreamBaseUrl = DEFAULT_UPSTREAM_BASE_URL, maxContextTokens = maxContextFromEnv(), rateLimiter = createRateLimiter(), pagesDirectory = defaultPagesDirectory } = {}) {
const rotator = createKeyRotator(keys);
const app = express();
app.use(express.json({ limit: '10mb' }));
app.get('/', (_request, response) => {
try {
const markdown = fs.readFileSync(path.join(pagesDirectory, 'index.md'), 'utf8');
response.type('html').send(renderPage(markdown));
} catch (error) {
response.status(404).send('Page not found');
}
});
async function fetchWithKeyRetries(url, options = {}) {
let lastError;
+13
View File
@@ -0,0 +1,13 @@
import { describe, expect, it } from 'vitest';
import request from 'supertest';
import { createProxyApp } from '../src/proxy.js';
describe('pages', () => {
it('renders the Markdown home page at /', async () => {
const result = await request(createProxyApp({ keys: ['key'] })).get('/').expect(200);
expect(result.headers['content-type']).toMatch(/text\/html/);
expect(result.text).toContain('<h1>OpenCode Go Proxy</h1>');
expect(result.text).toContain('OpenAI-compatible API');
});
});