Instruction file imported from mergente/ts-express-ai (
.github/instructions/ai-models.instructions.md). Copyright stays with the author.
AI Models Standards
One File Per Provider
ENFORCE: All AI provider logic lives in src/models/<provider>.ts
src/models/
gemini.ts → @google/generative-ai
openai.ts → openai
huggingface.ts → @huggingface/inference
replicate.ts → replicate
FLAG FOR REVIEW: AI provider code added to src/index.ts
// WRONG — provider logic in the route file
app.post('/make-image', async (req, res) => {
const openai = new OpenAI({});
const response = await openai.images.generate({ ... });
})
// CORRECT — provider logic in src/models/openai.ts, imported into index.ts
import { makeImage } from './models/openai'
app.post('/make-image', async (req, res) => {
const image = await makeImage(req.body.promptRephrase);
})
ENFORCE: Named exports only — no default exports from model files
// CORRECT
export { openai, makePrompt, makeImage }
export { hfImage }
export { replicateImage, replicateVideo }
// WRONG
export default makeImage
Client Initialization at Module Scope
ENFORCE: Clients initialized once at module scope, not inside functions
// CORRECT
const openai = new OpenAI({})
const genAI = new GoogleGenerativeAI(GEMINI_TOKEN)
const hf = new HfInference(HF_API_TOKEN)
const replicate = new Replicate({ auth: process.env.REPLICATE_API_TOKEN })
async function makeImage(prompt: string) {
// use the already-initialized client
return await openai.images.generate({ ... })
}
FLAG FOR REVIEW: Client created inside a function body
// WRONG — creates a new client on every call
async function makeImage(prompt: string) {
const openai = new OpenAI({});
return await openai.images.generate({ ... });
}
Model Constants at Module Scope
ENFORCE: Model name strings as module-level constants, never hardcoded inside function calls
// CORRECT — gemini.ts
const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' })
// CORRECT — huggingface.ts (snake_case for HuggingFace model variants)
const model_stable = 'stabilityai/stable-diffusion-3-medium-diffusers'
// CORRECT — replicate.ts (SCREAMING_SNAKE_CASE for version-pinned strings)
const IMG_MODEL = 'adirik/flux-cinestill:216a43b9975de9768114644bbf8cd0cba54a923c6d0f65adceaccfc9383a938f'
const VIDEO_MODEL = 'cjwbw/videocrafter:02edcff3e9d2d11dcc27e530773d988df25462b1ee93ed0257b6f246de4797c8'
FLAG FOR REVIEW: Model string hardcoded inside a function or API call
// WRONG
async function makeImage(prompt: string) {
return await openai.images.generate({
model: 'dall-e-3', // should be a module-level const
...
})
}
Replicate Version Pinning
ENFORCE: Replicate model identifiers must include a full SHA version string
// CORRECT — SHA pinned
const IMG_MODEL = 'adirik/flux-cinestill:216a43b9975de9768114644bbf8cd0cba54a923c6d0f65adceaccfc9383a938f'
// WRONG — floating reference
const IMG_MODEL = 'adirik/flux-cinestill:latest'
const IMG_MODEL = 'adirik/flux-cinestill'
ENFORCE: Arrow function syntax for Replicate wrapper functions
// CORRECT
const replicateImage = async (prompt: string) => {
return await replicate.run(IMG_MODEL, { input: { prompt, ... } })
}
// WRONG — function declaration syntax breaks the convention
async function replicateImage(prompt: string) { ... }
System Prompts as Module Constants
ENFORCE: System prompts defined as module-level const, not embedded in API call arguments
// CORRECT — gemini.ts
const starterPrompt = `
You are an expert in prompt crafting.
Use the text input to craft a detailed prompt for image generation.
Keep the prompt length under 900 characters: `
async function makeImagePrompt(userPrompt: string) {
const prompt = `${starterPrompt} ${userPrompt}`
const { stream } = await model.generateContentStream(prompt)
return stream;
}
FLAG FOR REVIEW: Prompt string inlined directly in the API call
// WRONG — prompt instruction is buried inside the call
const result = await model.generateContent(`
You are an expert in prompt crafting. ${userPrompt}
`)
ENFORCE: OpenAI system prompts use the role: 'system' message slot
// CORRECT
messages: [
{ role: 'system', content: SYSTEM_PROMPT },
{ role: 'user', content: userInput }
]
Gemini Text and Streaming
ENFORCE: Use generateContentStream when the route needs to SSE-stream chunks
// CORRECT — streaming variant returns the stream object
async function makeImagePrompt(userPrompt: string) {
const { stream } = await model.generateContentStream(prompt)
return stream;
}
FLAG FOR REVIEW: Gemini used for image generation
Gemini (@google/generative-ai) handles text generation and prompt rephrasing only. It cannot generate images. Route image generation to openai.ts (DALL-E 3), huggingface.ts (Stable Diffusion), or replicate.ts (Flux Cinestill).
ENFORCE: Check chunk.candidates before treating a chunk as final
// CORRECT
for await (const chunk of promptStream) {
if (chunk.candidates) {
if (chunk.candidates[0].finishReason === 'STOP') {
// final chunk handling
}
sendEvent({ promptResponse: chunk.text() });
}
}
OpenAI Chat and Image
ENFORCE: new OpenAI({}) with empty options object — SDK reads key from environment automatically
// CORRECT
const openai = new OpenAI({})
// WRONG — passing key explicitly (unnecessary and risks accidental exposure)
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
ENFORCE: Streaming chat completions require both stream: true and stream_options
// CORRECT
const chatStream = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [...],
stream: true,
stream_options: { "include_usage": true },
})
ENFORCE: makeImage returns response.data[0].url — a string
// CORRECT — returns the URL string
async function makeImage(prompt: string) {
const response = await openai.images.generate({ model: 'dall-e-3', n: 1, prompt })
return response.data[0].url
}
Hugging Face — Blob to Disk
ENFORCE: Explicit cast of textToImage response to Blob
// CORRECT — SDK types the return loosely
const blob = imgReq as Blob;
ENFORCE: Use blobToBuffer helper for Blob → Buffer conversion; do not inline arrayBuffer()
// CORRECT
const buffer = await blobToBuffer(blob)
// WRONG — inlining the conversion
const buffer = Buffer.from(await imgReq.arrayBuffer())
ENFORCE: Timestamp format for HuggingFace image filenames
// CORRECT — consistent, filesystem-safe format
const timestamp = new Date().toISOString()
.replace(/[:.]/g, '-').replace('T', '_').split('.')[0];
await storeImage(blob, `hf-image-stable_${timestamp}.jpg`)
ENFORCE: imgUrl paths must start with /assets/ to resolve as static assets
// CORRECT
return { imgUrl: `/assets/images/hf-image-stable_${timestamp}.jpg`, altText: prompt }
// WRONG — absolute filesystem path exposed as URL
return { imgUrl: `/Users/edgarquintanilla/Sites/.../hf-image-stable.jpg` }
ENFORCE: storeImage resolves the image root with path.join(__dirname, '..', 'public', 'assets', 'images')
// CORRECT
const imageRoot = path.join(__dirname, '..', 'public', 'assets', 'images')
// WRONG — hardcoded absolute path
const imageRoot = '/Users/edgarquintanilla/Sites/mergente/ts-express-ai/src/public/assets/images'
Code Review Checklist
ENFORCE:
- All AI provider logic (client init, API calls, prompts) lives in
src/models/<provider>.ts— not insrc/index.ts - Named exports only — no
export defaultfrom model files - AI clients initialized at module scope, not inside function bodies
- Model name strings are module-level constants — never hardcoded in function calls
- Replicate model identifiers include a full SHA version string
- Arrow function syntax for all Replicate wrapper functions
- System prompts defined as module-level
const, not embedded in API calls - OpenAI initialized as
new OpenAI({})with empty options - Streaming OpenAI calls include both
stream: trueandstream_options: { "include_usage": true } makeImagereturnsresponse.data[0].url(string)- HuggingFace
textToImageresponse explicitly cast toBlob - HuggingFace
imgUrlreturn value starts with/assets/(not a filesystem path) storeImageusespath.join(__dirname, '..', 'public', 'assets', 'images')for the image root
FLAG FOR REVIEW:
- Gemini called for image generation — it handles text/streaming only
chunk.candidatesnot checked before accessingfinishReasonin Gemini stream loops- HuggingFace Blob → Buffer conversion inlined instead of using the
blobToBufferhelper - Replicate model identifier without a SHA pin (floating reference)
- New provider added without a corresponding file in
src/models/and entry insrc/models/README.md