From 0e9131dafa835d9f7f189855d57dfa18fd164a9b Mon Sep 17 00:00:00 2001 From: Owen Qwen Date: Tue, 21 Jul 2026 10:53:02 -0500 Subject: [PATCH] adding home page --- pages/index.md | 10 +++++ src/markdown.js | 92 +++++++++++++++++++++++++++++++++++++++++++ src/proxy.js | 16 +++++++- test/markdown.test.js | 13 ++++++ 4 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 pages/index.md create mode 100644 src/markdown.js create mode 100644 test/markdown.test.js diff --git a/pages/index.md b/pages/index.md new file mode 100644 index 0000000..2cf8faf --- /dev/null +++ b/pages/index.md @@ -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. diff --git a/src/markdown.js b/src/markdown.js new file mode 100644 index 0000000..31cbb29 --- /dev/null +++ b/src/markdown.js @@ -0,0 +1,92 @@ +function escapeHtml(value) { + return value + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +function inlineMarkdown(value) { + let html = escapeHtml(value); + html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '$1'); + html = html.replace(/`([^`]+)`/g, '$1'); + html = html.replace(/\*\*([^*]+)\*\*/g, '$1'); + 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(`

${inlineMarkdown(paragraph.join(' '))}

`); + paragraph = []; + } + }; + const flushList = () => { + if (list.length) { + output.push(``); + list = []; + } + }; + + for (const line of lines) { + if (code) { + if (line.startsWith('```')) { + output.push(`
${escapeHtml(code.join('\n'))}
`); + 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(`${inlineMarkdown(heading)}`); + } 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(`
${escapeHtml(code.join('\n'))}
`); + flushParagraph(); + flushList(); + return output.join('\n'); +} + +export function renderPage(markdown, { title = 'OpenCode Go Proxy' } = {}) { + return ` + + + + + ${escapeHtml(title)} + + + ${renderMarkdown(markdown)} +`; +} diff --git a/src/proxy.js b/src/proxy.js index f75aa6d..a27f18d 100644 --- a/src/proxy.js +++ b/src/proxy.js @@ -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; diff --git a/test/markdown.test.js b/test/markdown.test.js new file mode 100644 index 0000000..c6b6671 --- /dev/null +++ b/test/markdown.test.js @@ -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('

OpenCode Go Proxy

'); + expect(result.text).toContain('OpenAI-compatible API'); + }); +});