From 42409c3d5af620e408fdefe40a8dfda7021dff2a Mon Sep 17 00:00:00 2001 From: dab Date: Sun, 23 Nov 2025 11:46:51 +0000 Subject: [PATCH] Version 0.1.0 - initial commit --- README.md | 21 +++++ backend/index.js | 217 +++++++++++++++++++++++++++++++++++++++++++ backend/mergeDeep.js | 35 +++++++ backend/package.json | 5 + package.json | 6 ++ 5 files changed, 284 insertions(+) create mode 100644 README.md create mode 100644 backend/index.js create mode 100644 backend/mergeDeep.js create mode 100644 backend/package.json create mode 100644 package.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d8798f --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# aiprox + +## getting started + +Clone repo: + +```bash +bun clone 'git+https://git.daemons.my/dab/aiprox.git' +``` + +Install backend dependencies: + +```bash +cd aiprox/backend && bun install +```` + +Run server: + +```bash +bun run aiprox/backend/index.js $PATH_TO_TEMPLATES $PATH_TO_PROVIDERS +``` diff --git a/backend/index.js b/backend/index.js new file mode 100644 index 0000000..31b88e6 --- /dev/null +++ b/backend/index.js @@ -0,0 +1,217 @@ +import { async_run } from 'framerock' + +import { mergeDeep } from './mergeDeep' + +//============================================// +const [ path_templates, path_providers ] = Bun.argv.slice(2) + +const file_templates = Bun.file(path_templates) +const file_providers = Bun.file(path_providers) + +const PROMPT_TEMPLATES = await file_templates.json() +const PROVIDERS = await file_providers.json() +//============================================// + +//============================================// +const handle_fetch_fallback = function (req) { + + let resp + + const url = new URL(req.url) + + if ( + req.method === 'POST' && + url.pathname === '/evaluate_template' + ) { + + resp = new Response( + async function* () { + + try { + + let messages + let response_format + //============================================// + const { prompt_template, template_parts, merge_schema } = await req.json() + + const def_template = PROMPT_TEMPLATES[prompt_template] + + if (def_template !== undefined) { + + let provider_uid + let model + + //============================================// + const { provider_model_strategy, schema, template } = def_template + + const [ strategy_type, strategy_opts ] = provider_model_strategy + + if (strategy_type === 'strict') { + provider_uid = strategy_opts.provider_uid + model = strategy_opts.model + } + else { + throw `unexpected strategy_type : ${strategy_type}` + } + + const json_schema = mergeDeep(merge_schema, schema) + const template_slots = [ '\n' + JSON.stringify(json_schema) + '\n\n', ...template ] + + const copy_parts = [...template_parts] + + const full_parts = [] + for (let x of template_slots) { + if (x !== null) { + full_parts.push(x) + } + else { + full_parts.push(copy_parts.shift()) + } + } + + const content = full_parts.join('\n') + + messages = [ + { + "role": "user", + "content": content, + } + ] + + response_format = { + "type": "json_schema", + "json_schema": { + "name": "chat_response", + "strict": true, + "schema": json_schema, + } + } + //============================================// + + //============================================// + // !! NOTE: have to send dummy char every so often to prevent connection timeout !! + + let result_obj + let did_error + + handle_chat_completion(provider_uid, model, { messages, response_format }).then((resp_json) => { + if (resp_json === undefined) { + did_error = true + } + else { + const result_str = resp_json.choices[0].message.content + result_obj = JSON.parse(result_str) + } + return + }).catch(console.error) + + let last_yield = Date.now() + + while ( + result_obj === undefined && + did_error === undefined + ) { + await Bun.sleep(100) + if ((Date.now() - last_yield) >= (5 * 1000)) { + yield ' ' + last_yield = Date.now() + } + } + + if (did_error !== undefined) { + yield JSON.stringify({ 'ERROR': true }) + } + else { + yield JSON.stringify(result_obj) + } + //============================================// + + } + else { + throw `unexpected prompt_template : ${prompt_template}` + } + //============================================// + + } + catch (err) { + + console.error(err) + + yield JSON.stringify({ 'ERROR': true }) + + } + + return + }, + { headers: { 'Content-Type': 'application/json' } }, + ) + + } + + return resp + +} +//============================================// + +//============================================// +const handle_chat_completion = async (uid, model, { messages, response_format }) => { + + let submsg_resp + + const params = { + model, + messages, + response_format, + } + const body = JSON.stringify(params) + + const provider_info = PROVIDERS[uid] + + if (provider_info === undefined) { + + console.error(`no matching provider : ${uid}`) + + } + else { + + const { url, auth_token } = provider_info + + const full_url = url + '/v1/chat/completions' + + let headers + if (auth_token !== undefined) { + headers = {'Authorization': `Bearer ${auth_token}`} + } + + const method = 'POST' + + const tstamp_before = Date.now() + const resp = await fetch( + full_url, + { + method, + headers: { 'Content-Type': 'application/json', ...headers }, + body, + } + ) + const tstamp_after = Date.now() + + const resp_text = await resp.text() + if (resp.status === 200) { + // NOTE: assumes valid JSON + submsg_resp = JSON.parse(resp_text) + } + else { + console.error(['unexpected status', resp.status]) + // DEBUG + console.info({ resp_text }) + } + + } + + return submsg_resp + +} +//============================================// + +async_run({ handle_fetch_fallback }).then(()=>{}).catch(console.error) diff --git a/backend/mergeDeep.js b/backend/mergeDeep.js new file mode 100644 index 0000000..ce9b582 --- /dev/null +++ b/backend/mergeDeep.js @@ -0,0 +1,35 @@ +// Source - https://stackoverflow.com/a/34749873 +// Posted by Salakar, modified by community. See post 'Timeline' for change history +// Retrieved 2025-11-22, License - CC BY-SA 3.0 + +/** + * Simple object check. + * @param item + * @returns {boolean} + */ +export function isObject(item) { + return (item && typeof item === 'object' && !Array.isArray(item)); +} + +/** + * Deep merge two objects. + * @param target + * @param ...sources + */ +export function mergeDeep(target, ...sources) { + if (!sources.length) return target; + const source = sources.shift(); + + if (isObject(target) && isObject(source)) { + for (const key in source) { + if (isObject(source[key])) { + if (!target[key]) Object.assign(target, { [key]: {} }); + mergeDeep(target[key], source[key]); + } else { + Object.assign(target, { [key]: source[key] }); + } + } + } + + return mergeDeep(target, ...sources); +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..f40aed7 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "framerock": "git+https://git.daemons.my/dab/framerock.git" + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..340a967 --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "name": "aiprox", + "version": "0.1.0", + "type": "module", + "main": "backend/index.js" +}