SPB Git

Repository Public Research preview

openai-api.mjs 5.4 KB 87 lines javascript

1#!/usr/bin/env node2/**3 * Offline companion for the openai-api-toolkit skill. No network, no API key, no dependencies.4 *5 *   node openai-api.mjs models [model-id]            model table, or everything known about one model6 *   node openai-api.mjs lint <request.json|->        findings for a request (exit 2 if the API would reject it)7 *   node openai-api.mjs fix <request.json|->         corrected request + list of changes8 *   node openai-api.mjs convert <chat-request.json|->   Chat Completions -> Responses API9 *   node openai-api.mjs migrate <request.json|-> <target-model>10 *   node openai-api.mjs cost <model> <input_tokens> <output_tokens> [--cached N] [--batch] [--fast] [--requests N]11 *   node openai-api.mjs compare <input_tokens> <output_tokens> [--requests N]12 *   node openai-api.mjs scaffold <approach> [--language typescript|python] [--model ID] [--out FILE]13 *   node openai-api.mjs guide [topic]14 * The API (responses or chat) is detected from the body; force it with --api responses|chat. Output is JSON unless noted.15 */16import { readFileSync, writeFileSync, existsSync } from "node:fs";17import { compareCost, estimateCost } from "./lib/cost.mjs";18import { GUIDES } from "./lib/guides.mjs";19import { KB, MODELS, RECOMMENDED, modelSummary, resolveModel } from "./lib/knowledge.mjs";20import { chatToResponses, detectApi, fixRequest, lintRequest } from "./lib/lint.mjs";21import { APPROACHES, scaffold } from "./lib/scaffold.mjs";2223const [cmd, ...rest] = process.argv.slice(2);24const flags = {}, args = [];25for (let i = 0; i < rest.length; i++) {26  if (rest[i].startsWith("--")) { const k = rest[i].slice(2); const v = rest[i + 1]; if (v === undefined || v.startsWith("--")) flags[k] = true; else { flags[k] = v; i++; } }27  else args.push(rest[i]);28}29const print = (x) => process.stdout.write(JSON.stringify(x, null, 2) + "\n");30const die = (msg, code = 1) => { process.stderr.write(`error: ${msg}\n`); process.exit(code); };31function readRequest(src) {32  if (!src) die("give a request file, or - for stdin");33  let text;34  try { text = src === "-" ? readFileSync(0, "utf8") : readFileSync(src, "utf8"); } catch { die(`cannot read ${src}`); }35  try { return JSON.parse(text); } catch (e) { die(`${src} is not valid JSON: ${e.message}`); }36}37const opts = { api: flags.api === "responses" || flags.api === "chat" ? flags.api : undefined };38const int = (v, name) => { const n = Number(v); if (!Number.isInteger(n) || n < 0) die(`${name} must be a non-negative integer`); return n; };3940switch (cmd) {41  case "models": {42    if (args[0]) { const m = resolveModel(args[0]); if (!m) die(`unknown model "${args[0]}". Known: ${Object.keys(MODELS).join(", ")}`); print({ observed_at: KB.observed_at, ...m }); }43    else print({ observed_at: KB.observed_at, recommended: RECOMMENDED, models: Object.values(MODELS).map(modelSummary) });44    break;45  }46  case "lint": { const r = lintRequest(readRequest(args[0]), opts); print(r); process.exit(r.valid ? 0 : 2); }47  case "fix": { print(fixRequest(readRequest(args[0]), opts)); break; }48  case "convert": { print(chatToResponses(readRequest(args[0]))); break; }49  case "migrate": {50    const target = resolveModel(args[1]);51    if (!target) die(`unknown target model "${args[1]}"`);52    const req = { ...readRequest(args[0]), model: target.id };53    // A Chat Completions request cannot run on a Responses-only model: convert it instead of leaving it invalid.54    const chatOnly = (opts.api ?? detectApi(req)) === "chat" && !target.chat_completions.supported;55    print(chatOnly ? { to: target.id, converted_to: "responses", ...chatToResponses(req) } : { to: target.id, ...fixRequest(req, opts) });56    break;57  }58  case "cost": {59    if (args.length < 3) die("usage: cost <model> <input_tokens> <output_tokens>");60    const r = estimateCost({ model: args[0], input_tokens: int(args[1], "input_tokens"), output_tokens: int(args[2], "output_tokens"), cached_input_tokens: flags.cached ? int(flags.cached, "--cached") : 0, batch: !!flags.batch, fast: !!flags.fast, requests: flags.requests ? int(flags.requests, "--requests") : 1 });61    if (r.error) die(r.error);62    print(r);63    break;64  }65  case "compare": {66    if (args.length < 2) die("usage: compare <input_tokens> <output_tokens>");67    print(compareCost({ input_tokens: int(args[0], "input_tokens"), output_tokens: int(args[1], "output_tokens"), requests: flags.requests ? int(flags.requests, "--requests") : 1, batch: !!flags.batch }, Object.keys(MODELS).filter((id) => MODELS[id].pricing_usd_per_mtok)));68    break;69  }70  case "scaffold": {71    const r = scaffold({ approach: args[0], language: flags.language ?? "typescript", model: flags.model, effort: flags.effort });72    if (r.error) die(`${r.error}\napproaches: ${Object.entries(APPROACHES).map(([k, v]) => `\n  ${k} — ${v}`).join("")}`);73    if (flags.out) { if (existsSync(flags.out)) die(`${flags.out} already exists; refusing to overwrite`); writeFileSync(flags.out, r.code); process.stderr.write(`wrote ${flags.out}\ninstall: ${r.install}\nrun:     ${r.run}\n`); }74    else process.stdout.write(r.code);75    break;76  }77  case "guide": {78    if (!args[0]) print({ topics: Object.keys(GUIDES) });79    else if (!GUIDES[args[0]]) die(`unknown topic "${args[0]}". Topics: ${Object.keys(GUIDES).join(", ")}`);80    else process.stdout.write(GUIDES[args[0]] + "\n");81    break;82  }83  default:84    process.stderr.write(readFileSync(new URL(import.meta.url), "utf8").split("*/")[0].split("\n").slice(2).map((l) => l.replace(/^ \* ?/, "")).join("\n") + "\n");85    process.exit(cmd ? 1 : 0);86}87
Files main
Menu

Theme