← Files Vocabloot Deck KitARCHIVED FILE
scripts/lib/openai.mjs
4.71 KB · Oct 2, 2026 · 00:34 UTC
// One small client interface over the OpenAI SDK, and a fixture client for
// tests and dry runs. Every call is keyed so a response can be recorded and
// replayed; the kit never touches any secret store but the creator's key.
import { createHash } from "node:crypto";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { AgentClient } from "./agent.mjs";
export const DEFAULT_MODEL = "gpt-4.1-mini";
export const IMAGE_MODEL = "gpt-image-1";
function fixtureName(key) {
return createHash("sha256").update(key).digest("hex").slice(0, 24) + ".json";
}
/** Replays recorded responses from a folder; throws with the key when one is missing. */
export class FixtureClient {
constructor(dir) {
this.dir = dir;
}
read(key) {
const file = path.join(this.dir, fixtureName(key));
if (!existsSync(file))
throw new Error(
`No recorded response for "${key}" (expected ${path.relative(process.cwd(), file)}). Run with a real key and --record to create it.`,
);
return JSON.parse(readFileSync(file, "utf8")).response;
}
async chatJson({ key }) {
return this.read(key);
}
async image({ key }) {
return this.read(key);
}
async vision({ key }) {
return this.read(key);
}
}
/** The real client; with `record`, every response is also written as a fixture. */
export class OpenAIClient {
constructor({ apiKey, record = null, sdk = null }) {
this.apiKey = apiKey;
this.record = record;
this.sdk = sdk;
}
async client() {
if (!this.sdk) {
const { default: OpenAI } = await import("openai");
this.sdk = new OpenAI({ apiKey: this.apiKey });
}
return this.sdk;
}
save(key, request, response) {
if (!this.record) return;
mkdirSync(this.record, { recursive: true });
writeFileSync(path.join(this.record, fixtureName(key)), JSON.stringify({ key, request, response }, null, 2) + "\n");
}
/** A strict JSON-schema chat call; returns the parsed object. */
async chatJson({ key, model = DEFAULT_MODEL, system, user, schema, schemaName, temperature = 0, seed = 7 }) {
const sdk = await this.client();
const request = {
model,
temperature,
seed,
messages: [
{ role: "system", content: system },
{ role: "user", content: user },
],
response_format: { type: "json_schema", json_schema: { name: schemaName, strict: true, schema } },
};
const completion = await sdk.chat.completions.create(request);
const text = completion.choices?.[0]?.message?.content ?? "";
const response = JSON.parse(text);
this.save(key, { model, system, user, schemaName }, response);
return response;
}
/** One transparent square image; returns { pngBase64 }. */
async image({ key, prompt, model = IMAGE_MODEL, size = "1024x1024", quality = "medium" }) {
const sdk = await this.client();
const result = await sdk.images.generate({
model,
prompt,
size,
quality,
background: "transparent",
output_format: "png",
n: 1,
});
const response = { pngBase64: result.data?.[0]?.b64_json ?? "" };
this.save(key, { model, prompt, size }, response);
return response;
}
/** Asks the model whether an image depicts a concept; returns { depicts, reason }. */
async vision({ key, model = DEFAULT_MODEL, prompt, pngBase64 }) {
const sdk = await this.client();
const schema = {
type: "object",
additionalProperties: false,
required: ["depicts", "reason"],
properties: { depicts: { type: "boolean" }, reason: { type: "string" } },
};
const completion = await sdk.chat.completions.create({
model,
temperature: 0,
messages: [
{
role: "user",
content: [
{ type: "text", text: prompt },
{ type: "image_url", image_url: { url: `data:image/png;base64,${pngBase64}` } },
],
},
],
response_format: { type: "json_schema", json_schema: { name: "vision_check", strict: true, schema } },
});
const response = JSON.parse(completion.choices?.[0]?.message?.content ?? "{}");
this.save(key, { model, prompt }, response);
return response;
}
}
/**
* Picks the client: fixtures when OPENAI_FIXTURES points at a folder; the agent running the
* skill when there is no key (or `agent` is set); else the real API with the creator's key.
*/
export function makeClient({
apiKey,
fixtures = process.env.OPENAI_FIXTURES,
record = process.env.OPENAI_RECORD,
agent = false,
workDir = null,
} = {}) {
if (fixtures) return new FixtureClient(fixtures);
if (agent || !apiKey) return new AgentClient(workDir ?? path.join(process.cwd(), "work"));
return new OpenAIClient({ apiKey, record });
}
SHA-256: 9b3f842f3a95620fec237e7c55d4f055eeda3e819aacd5e990f01029157c039b