inital commit

This commit is contained in:
2026-07-21 01:06:08 -05:00
commit fafc502aeb
10 changed files with 3336 additions and 0 deletions
+194
View File
@@ -0,0 +1,194 @@
const SUPPORTED_IMAGE_TYPES = new Set(['image/jpeg', 'image/png', 'image/gif', 'image/webp']);
function anthropicError(message, type = 'invalid_request_error') {
const error = new Error(message);
error.status = 400;
error.body = { type: 'error', error: { type, message } };
return error;
}
function textBlock(block) {
if (typeof block === 'string') return block;
if (block?.type === 'text' && typeof block.text === 'string') return block.text;
throw anthropicError(`Unsupported content block type: ${block?.type || 'unknown'}`);
}
function imageBlock(block) {
const source = block?.source;
if (block?.type !== 'image' || source?.type !== 'base64' || !SUPPORTED_IMAGE_TYPES.has(source.media_type)) {
throw anthropicError('Only base64 JPEG, PNG, GIF, and WebP image blocks are supported');
}
return { type: 'image_url', image_url: { url: `data:${source.media_type};base64,${source.data}` } };
}
function toOpenAIContent(content) {
if (typeof content === 'string') return content;
if (!Array.isArray(content)) throw anthropicError('Message content must be a string or array of content blocks');
return content.map((block) => {
if (block?.type === 'text') return { type: 'text', text: textBlock(block) };
if (block?.type === 'image') return imageBlock(block);
throw anthropicError(`Unsupported content block type: ${block?.type || 'unknown'}`);
});
}
function toOpenAITool(tool) {
if (!tool || tool.type === 'custom' || !tool.name || !tool.input_schema) {
throw anthropicError('Tools must include a name and input_schema');
}
return {
type: 'function',
function: { name: tool.name, description: tool.description, parameters: tool.input_schema },
};
}
export function translateAnthropicRequest(body = {}) {
if (!body.model) throw anthropicError('model is required');
if (!Number.isFinite(body.max_tokens)) throw anthropicError('max_tokens is required');
if (!Array.isArray(body.messages)) throw anthropicError('messages must be an array');
const messages = [];
if (body.system !== undefined) {
messages.push({ role: 'system', content: toOpenAIContent(body.system) });
}
for (const message of body.messages) {
if (message?.role === 'system' || message?.role === 'developer') {
messages.push({ role: 'system', content: toOpenAIContent(message.content) });
continue;
}
if (message?.role === 'tool') {
messages.push({ role: 'tool', tool_call_id: message.tool_call_id || message.tool_use_id, content: toOpenAIContent(message.content || '') });
continue;
}
if (!['user', 'assistant'].includes(message?.role)) throw anthropicError(`Unsupported message role: ${message?.role || 'missing'}`);
if (!Array.isArray(message.content)) {
messages.push({ role: message.role, content: toOpenAIContent(message.content) });
continue;
}
const ordinary = message.content.filter((block) => block?.type !== 'tool_result' && block?.type !== 'tool_use' && block?.type !== 'thinking');
const thinking = message.content.filter((block) => block?.type === 'thinking');
if (thinking.length && message.role !== 'assistant') throw anthropicError('thinking blocks are only valid in assistant messages');
if (ordinary.length || thinking.length) {
const converted = { role: message.role, content: ordinary.length ? toOpenAIContent(ordinary) : null };
if (thinking.length) {
if (thinking.some((block) => typeof block.thinking !== 'string')) throw anthropicError('thinking blocks must include thinking text');
converted.reasoning_content = thinking.map((block) => block.thinking).join('');
}
messages.push(converted);
}
for (const block of message.content) {
if (block.type === 'tool_result') {
if (!block.tool_use_id) throw anthropicError('tool_result must include tool_use_id');
messages.push({ role: 'tool', tool_call_id: block.tool_use_id, content: toOpenAIContent(block.content || '') });
} else if (block.type === 'tool_use') {
if (!block.id || !block.name) throw anthropicError('tool_use must include id and name');
const assistant = messages.at(-1)?.role === 'assistant' ? messages.at(-1) : null;
if (assistant) {
assistant.tool_calls = assistant.tool_calls || [];
assistant.tool_calls.push({ id: block.id, type: 'function', function: { name: block.name, arguments: JSON.stringify(block.input ?? {}) } });
} else {
messages.push({ role: 'assistant', content: null, tool_calls: [{ id: block.id, type: 'function', function: { name: block.name, arguments: JSON.stringify(block.input ?? {}) } }] });
}
}
}
}
const result = {
model: body.model,
messages,
max_tokens: body.max_tokens,
};
for (const field of ['temperature', 'top_p']) if (body[field] !== undefined) result[field] = body[field];
if (body.stop_sequences !== undefined) result.stop = body.stop_sequences;
if (body.stream !== undefined) result.stream = body.stream;
if (body.tools !== undefined) result.tools = body.tools.map(toOpenAITool);
if (body.tool_choice !== undefined) {
if (body.tool_choice.type === 'auto') result.tool_choice = 'auto';
else if (body.tool_choice.type === 'any') result.tool_choice = 'required';
else if (body.tool_choice.type === 'tool' && body.tool_choice.name) result.tool_choice = { type: 'function', function: { name: body.tool_choice.name } };
else throw anthropicError('Unsupported tool_choice');
}
return result;
}
function usageOf(usage = {}) {
return { input_tokens: usage.prompt_tokens ?? 0, output_tokens: usage.completion_tokens ?? 0 };
}
export function translateAnthropicResponse(payload, requestedModel) {
const choice = payload.choices?.[0] || {};
const message = choice.message || {};
const content = [];
if (message.reasoning_content) content.push({ type: 'thinking', thinking: message.reasoning_content });
if (message.content) content.push({ type: 'text', text: message.content });
for (const call of message.tool_calls || []) {
let input = {};
try { input = JSON.parse(call.function?.arguments || '{}'); } catch { input = {}; }
content.push({ type: 'tool_use', id: call.id, name: call.function?.name, input });
}
const finish = choice.finish_reason;
return {
id: payload.id || `msg_${Date.now()}`,
type: 'message', role: 'assistant', model: payload.model || requestedModel,
content, stop_reason: finish === 'tool_calls' ? 'tool_use' : finish === 'length' ? 'max_tokens' : finish === 'stop' ? 'end_turn' : null,
stop_sequence: null, usage: usageOf(payload.usage),
};
}
export function translateAnthropicModels(payload) {
return {
object: 'list',
data: (payload.data || []).map((model) => ({
id: model.id, display_name: model.name || model.id, created_at: new Date((model.created || 0) * 1000 || Date.now()).toISOString(), type: 'model',
})),
has_more: false, first_id: payload.data?.[0]?.id, last_id: payload.data?.at(-1)?.id,
};
}
function event(type, data) { return `event: ${type}\ndata: ${JSON.stringify(data)}\n\n`; }
export function translateOpenAIChunk(chunk, state) {
const choice = chunk.choices?.[0];
if (!choice) return '';
const delta = choice.delta || {};
let output = '';
if (!state.started) {
state.started = true;
output += event('message_start', { type: 'message_start', message: { id: state.id, type: 'message', role: 'assistant', model: state.model, content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 0, output_tokens: 0 } } });
}
if (delta.reasoning_content) {
if (!state.reasoningStarted) {
state.reasoningStarted = true;
state.reasoningIndex = state.nextBlockIndex++;
output += event('content_block_start', { type: 'content_block_start', index: state.reasoningIndex, content_block: { type: 'thinking', thinking: '' } });
}
output += event('content_block_delta', { type: 'content_block_delta', index: state.reasoningIndex, delta: { type: 'thinking_delta', thinking: delta.reasoning_content } });
}
if (delta.content) {
if (!state.contentStarted) {
state.contentStarted = true;
state.contentIndex = state.nextBlockIndex++;
output += event('content_block_start', { type: 'content_block_start', index: state.contentIndex, content_block: { type: 'text', text: '' } });
}
output += event('content_block_delta', { type: 'content_block_delta', index: state.contentIndex, delta: { type: 'text_delta', text: delta.content } });
}
if (delta.tool_calls?.length) {
const call = delta.tool_calls[0];
if (!state.toolStarted) {
state.toolStarted = true;
state.toolIndex = state.nextBlockIndex++;
output += event('content_block_start', { type: 'content_block_start', index: state.toolIndex, content_block: { type: 'tool_use', id: call.id || 'tool_use', name: call.function?.name || '', input: {} } });
}
if (call.function?.arguments) output += event('content_block_delta', { type: 'content_block_delta', index: state.toolIndex, delta: { type: 'input_json_delta', partial_json: call.function.arguments } });
}
if (choice.finish_reason) {
for (const index of [state.reasoningIndex, state.contentIndex, state.toolIndex]) {
if (index !== undefined) output += event('content_block_stop', { type: 'content_block_stop', index });
}
output += event('message_delta', { type: 'message_delta', delta: { stop_reason: choice.finish_reason === 'tool_calls' ? 'tool_use' : choice.finish_reason === 'length' ? 'max_tokens' : 'end_turn', stop_sequence: null }, usage: { output_tokens: chunk.usage?.completion_tokens || 0 } });
output += event('message_stop', { type: 'message_stop' });
}
return output;
}
export { anthropicError, event };
+34
View File
@@ -0,0 +1,34 @@
import fs from 'node:fs/promises';
export function parseKeys(contents) {
return contents
.split(/\r?\n/)
.map((line) => line.replace(/#.*/, '').trim())
.filter(Boolean);
}
export async function loadKeys(path = new URL('../keys.txt', import.meta.url)) {
const contents = await fs.readFile(path, 'utf8');
const keys = parseKeys(contents);
if (keys.length === 0) {
throw new Error(`No usable OpenCode Go API keys found in ${path.pathname ?? path}`);
}
return keys;
}
export function createKeyRotator(keys) {
if (!Array.isArray(keys) || keys.length === 0) {
throw new Error('At least one OpenCode Go API key is required');
}
let index = 0;
return {
next() {
const key = keys[index];
index = (index + 1) % keys.length;
return key;
},
};
}
+160
View File
@@ -0,0 +1,160 @@
import { Readable } from 'node:stream';
import express from 'express';
import { createKeyRotator } from './key-store.js';
import {
anthropicError,
translateAnthropicModels,
translateAnthropicRequest,
translateAnthropicResponse,
translateOpenAIChunk,
} from './anthropic.js';
export const DEFAULT_UPSTREAM_BASE_URL = 'https://opencode.ai/zen/go/v1';
function copyResponseHeaders(upstream, response) {
const contentType = upstream.headers.get('content-type');
const cacheControl = upstream.headers.get('cache-control');
if (contentType) response.set('content-type', contentType);
if (cacheControl) response.set('cache-control', cacheControl);
}
async function forwardError(upstream, response) {
copyResponseHeaders(upstream, response);
const body = await upstream.text();
response.status(upstream.status).send(body || upstream.statusText);
}
export function createProxyApp({ keys, fetchImpl = globalThis.fetch, upstreamBaseUrl = DEFAULT_UPSTREAM_BASE_URL } = {}) {
const rotator = createKeyRotator(keys);
const app = express();
app.use(express.json({ limit: '10mb' }));
async function fetchWithKeyRetries(url, options = {}) {
let lastError;
for (let attempt = 0; attempt < keys.length; attempt += 1) {
try {
const upstream = await fetchImpl(url, {
...options,
headers: {
...options.headers,
authorization: `Bearer ${rotator.next()}`,
},
});
if (upstream.ok || attempt === keys.length - 1) return upstream;
lastError = new Error(`OpenCode Go returned ${upstream.status}`);
} catch (error) {
lastError = error;
if (attempt === keys.length - 1) throw error;
}
}
throw lastError;
}
app.get('/oai/v1/models', async (_request, response) => {
try {
const upstream = await fetchWithKeyRetries(`${upstreamBaseUrl}/models`, {
headers: { accept: 'application/json' },
});
if (!upstream.ok) return forwardError(upstream, response);
copyResponseHeaders(upstream, response);
response.status(upstream.status).send(await upstream.text());
} catch (error) {
response.status(502).json({ error: { message: `Unable to reach OpenCode Go: ${error.message}`, type: 'upstream_error' } });
}
});
app.get(['/ant/v1/models', '/ant/v1/v1/models'], async (_request, response) => {
try {
const upstream = await fetchWithKeyRetries(`${upstreamBaseUrl}/models`, { headers: { accept: 'application/json' } });
if (!upstream.ok) return forwardError(upstream, response);
const payload = JSON.parse(await upstream.text());
response.type('application/json').send(JSON.stringify(translateAnthropicModels(payload)));
} catch (error) {
response.status(502).json({ type: 'error', error: { type: 'upstream_error', message: `Unable to reach OpenCode Go: ${error.message}` } });
}
});
app.post('/oai/v1/chat/completions', async (request, response) => {
try {
const upstream = await fetchWithKeyRetries(`${upstreamBaseUrl}/chat/completions`, {
method: 'POST',
headers: {
'content-type': 'application/json',
accept: request.body?.stream ? 'text/event-stream' : 'application/json',
},
body: JSON.stringify(request.body),
});
if (!upstream.ok) return forwardError(upstream, response);
copyResponseHeaders(upstream, response);
if (request.body?.stream && upstream.body) {
response.status(upstream.status);
Readable.fromWeb(upstream.body).pipe(response);
return;
}
response.status(upstream.status).send(await upstream.text());
} catch (error) {
if (!response.headersSent) {
response.status(502).json({ error: { message: `Unable to reach OpenCode Go: ${error.message}`, type: 'upstream_error' } });
}
}
});
app.post(['/ant/v1/messages', '/ant/v1/v1/messages'], async (request, response) => {
let translated;
try {
translated = translateAnthropicRequest(request.body);
} catch (error) {
if (error.body) return response.status(error.status || 400).json(error.body);
return response.status(400).json({ type: 'error', error: { type: 'invalid_request_error', message: error.message } });
}
try {
const upstream = await fetchWithKeyRetries(`${upstreamBaseUrl}/chat/completions`, {
method: 'POST',
headers: { 'content-type': 'application/json', accept: translated.stream ? 'text/event-stream' : 'application/json' },
body: JSON.stringify(translated),
});
if (!upstream.ok) return forwardError(upstream, response);
if (!translated.stream) {
const payload = JSON.parse(await upstream.text());
return response.type('application/json').send(JSON.stringify(translateAnthropicResponse(payload, translated.model)));
}
if (!upstream.body) return response.status(502).json({ type: 'error', error: { type: 'upstream_error', message: 'Upstream returned no streaming body' } });
response.status(upstream.status).type('text/event-stream');
const reader = upstream.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
const state = { id: `msg_${Date.now()}`, model: translated.model, nextBlockIndex: 0 };
const writeEvents = (text) => {
for (const eventText of text.split('\n\n')) {
if (!eventText.trim()) continue;
const data = eventText.split('\n').find((line) => line.startsWith('data:'))?.slice(5).trim();
if (!data || data === '[DONE]') continue;
try { response.write(translateOpenAIChunk(JSON.parse(data), state)); } catch { /* ignore malformed upstream event */ }
}
};
while (true) {
const { done, value } = await reader.read();
buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
const parts = buffer.split('\n\n');
buffer = parts.pop() || '';
writeEvents(parts.join('\n\n'));
if (done) break;
}
writeEvents(buffer);
response.end();
} catch (error) {
if (!response.headersSent) response.status(502).json({ type: 'error', error: { type: 'upstream_error', message: `Unable to reach OpenCode Go: ${error.message}` } });
else response.end();
}
});
return app;
}
+13
View File
@@ -0,0 +1,13 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createProxyApp } from './proxy.js';
import { loadKeys } from './key-store.js';
const projectRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
const keys = await loadKeys(path.join(projectRoot, 'keys.txt'));
const app = createProxyApp({ keys });
const port = Number(process.env.PORT || 4005);
app.listen(port, '127.0.0.1', () => {
console.log(`OpenCode Go proxy listening at http://localhost:${port}/oai/v1`);
});