NodeTool’s OpenAI integration lives in one file:
packages/runtime/src/providers/openai-provider.ts (1399 lines).
The provider is registered under PROVIDER_IDS.OPENAI = "openai" in
packages/protocol/src/api-types.ts (line 860).
Audience: coding agents and contributors adding new OpenAI models.
TL;DR
| Model type | What to do |
|---|---|
Chat / LLM (e.g. gpt-5, o3) |
Nothing — the list is fetched live from https://api.openai.com/v1/models |
Image (e.g. gpt-image-3) |
Add one entry to getAvailableImageModels() (~line 249) and one pricing entry if quality-based |
Video (e.g. sora-3) |
Add one entry to getAvailableVideoModels() (~line 232) |
| TTS / ASR / Embedding | Add one entry to the matching static getter |
Where things live
| Concern | Path | Notes |
|---|---|---|
| Provider class | packages/runtime/src/providers/openai-provider.ts |
All model lists and API calls |
| Provider ID constant | packages/protocol/src/api-types.ts line 860 |
PROVIDER_IDS.OPENAI = "openai" |
| Provider registration | packages/runtime/src/providers/index.ts |
Imports and re-exports OpenAIProvider |
| Non-token pricing tiers | packages/runtime/src/providers/cost-calculator.ts lines 56–87 |
PRICING_TIERS object |
| Per-model tier mapping | packages/runtime/src/providers/cost-calculator.ts lines 94–113 |
MODEL_TO_TIER object |
| Quality-based image cost | packages/runtime/src/providers/cost-calculator.ts lines 368–392 |
calculateImageCost() |
How OpenAI models are defined
Chat / LLM models — dynamic
getAvailableLanguageModels() (line 178) makes a live GET request to
https://api.openai.com/v1/models using the stored API key, then maps every
returned id to a LanguageModel object:
// openai-provider.ts lines 178–203
async getAvailableLanguageModels(): Promise<LanguageModel[]> {
const response = await this._fetch("https://api.openai.com/v1/models", {
headers: { Authorization: `Bearer ${this.apiKey}` }
});
if (!response.ok) return [];
const payload = await response.json() as { data?: Array<{ id?: string }> };
return (payload.data ?? [])
.filter((row): row is { id: string } => typeof row.id === "string")
.map((row) => ({ id: row.id, name: row.id, provider: "openai" }));
}
A new chat model released by OpenAI appears in NodeTool automatically the next time the model list is refreshed — no code change needed.
Tool support is controlled by hasToolSupport() (line 174):
async hasToolSupport(model: string): Promise<boolean> {
return !(model.startsWith("o1") || model.startsWith("o3"));
}
If a new model needs to opt out of tool use, add its prefix here.
Image, video, TTS, ASR, and embedding models — static
These model types are returned from hardcoded lists inside the provider because
OpenAI’s /v1/models endpoint does not distinguish modalities. Each getter
returns an array of typed objects.
getAvailableImageModels() (line 249) currently lists four models:
gpt-image-2, gpt-image-1.5, gpt-image-1, gpt-image-1-mini.
getAvailableVideoModels() (line 232) lists sora-2 and sora-2-pro.
getAvailableTTSModels() (line 205), getAvailableASRModels() (line 222),
and getAvailableEmbeddingModels() (line 278) follow the same pattern.
Add a new image model
1. Add the entry to getAvailableImageModels()
Open packages/runtime/src/providers/openai-provider.ts and add to the array
returned at line 249. Match the shape of existing entries exactly:
// packages/runtime/src/providers/openai-provider.ts ~line 249
async getAvailableImageModels(): Promise<ImageModel[]> {
return [
{
id: "gpt-image-3", // OpenAI API model ID
name: "GPT Image 3", // display name shown in the UI
provider: "openai",
supportedTasks: ["text_to_image", "image_to_image"]
},
// ... existing entries ...
];
}
supportedTasks must be a subset of ["text_to_image", "image_to_image", "inpainting"] —
use whichever the model actually supports.
2. Add pricing if quality-based
calculateImageCost() in cost-calculator.ts (line 376) special-cases any
model whose ID contains "gpt-image" (excluding gpt-image-1.5) and routes it
through quality tiers defined in PRICING_TIERS (lines 57–60):
// cost-calculator.ts lines 57–60
imageGptLow: { costType: CostType.IMAGE_BASED, perImage: 0.011 },
imageGptMedium: { costType: CostType.IMAGE_BASED, perImage: 0.042 },
imageGptHigh: { costType: CostType.IMAGE_BASED, perImage: 0.167 },
If gpt-image-3 uses the same three-tier structure at different prices, add
new tiers and extend the qualityMap inside calculateImageCost() (line 377):
// cost-calculator.ts PRICING_TIERS — add new tiers
imageGpt3Low: { costType: CostType.IMAGE_BASED, perImage: 0.015 },
imageGpt3Medium: { costType: CostType.IMAGE_BASED, perImage: 0.060 },
imageGpt3High: { costType: CostType.IMAGE_BASED, perImage: 0.200 },
Then guard the existing qualityMap lookup to branch on model ID:
// cost-calculator.ts calculateImageCost()
if (modelId.toLowerCase().includes("gpt-image-3")) {
const qualityMap = { low: "imageGpt3Low", medium: "imageGpt3Medium", high: "imageGpt3High" };
// ...
}
If the model is flat-rate (no quality levels), add it to MODEL_TO_TIER
(line 94) instead:
// cost-calculator.ts MODEL_TO_TIER
"openai:gpt-image-3": "imageGptMedium", // or a new tier
3. Add pricing for non-image modalities (TTS / ASR)
New TTS or ASR models follow the same pattern as existing entries in
MODEL_TO_TIER (lines 96–101). Add "openai:<model-id>": "<tierName>" and,
if needed, a new tier object in PRICING_TIERS.
Add a new chat / LLM model
Usually you do not need to do anything. The live fetch covers all models in your account. Check the model is available in your tier at https://platform.openai.com/docs/models.
A code change is needed only for these cases:
Disable tool use for a new reasoning model prefix — extend hasToolSupport()
(line 174):
async hasToolSupport(model: string): Promise<boolean> {
return !(
model.startsWith("o1") ||
model.startsWith("o3") ||
model.startsWith("o4") // add new reasoning prefix here
);
}
Token-based pricing — chat models are priced via @pydantic/genai-prices
(imported in cost-calculator.ts line 19). That package is community-maintained
and tracks OpenAI pricing. If a brand-new model is missing from the catalog,
wait for a @pydantic/genai-prices release or pin an interim entry by adding a
dummy MODEL_TO_TIER key that maps to an existing tier.
Verify
Run these in order after any edit:
# 1. Type-check the runtime package (and all packages)
npm run typecheck
# 2. Smoke-test the model list (requires OPENAI_API_KEY in environment)
npm run dev:nodetool -- info
# 3. Smoke-test a single image node (no secrets needed for type check)
npm run dev:nodetool -- node run nodetool.image.generate.OpenAIImageNode \
--props '{"prompt": "a red circle", "model": {"id": "gpt-image-1", "provider": "openai"}}' \
--no-secrets
# 4. Full check (typecheck + lint + tests)
npm run check
All three of npm run typecheck, npm run lint, and npm run test must pass
before committing.
How past PRs did it
The XAI provider addition (commit 69dd6f88, PR #3951, “Add image and video
generation support to XAI provider”) is the closest parallel: it shows the exact
pattern for adding static image and video model lists to a provider that already
handles dynamic language model fetching. The files changed were
packages/runtime/src/providers/xai-provider.ts and
packages/runtime/src/providers/cost-calculator.ts — the same two files you
edit for a new OpenAI image model.
The OpenAI provider’s static video list (sora-2, sora-2-pro) was added
following the same approach, visible in getAvailableVideoModels() at line 232
of openai-provider.ts.
Contributing
Open a PR at https://github.com/nodetool-ai/nodetool. Before pushing:
npm run check # typecheck + lint + test across all workspaces
Join the discussion on Discord.