Build your own Ziwei (Purple Star Astrology / 紫微斗数) or Qimen agent in TypeScript.
npm install openai-iztro-agents→import { iztroZiweiAgent } from 'openai-iztro-agents'🐍 Prefer Python? See the sibling package openai-iztro-agents (Python) — same design, Pythonic API.
A thin layer on top of the OpenAI Agents SDK for JS/TS (@openai/agents):
- The hosted Ziwei and Qimen models and their astrology tools run on the server (hidden) — exposed as stock SDK models.
- Your own function tools, MCP servers, and human-in-the-loop run locally via the standard
run(). - Conversation memory lives on the server via
ChatSession(the OpenAI Conversations–style session).
You write ordinary OpenAI Agents SDK code — Agent, run, tool, @openai/agents MCP servers, modelSettings.toolChoice, needsApproval — and point the model at Ziwei or Qimen. This is the JS twin of the Python openai-iztro-agents: same design, different language.
npm install openai-iztro-agentsGet an API key (sk_ziwei_*) from the developer console.
Set it once in the server environment so the Agent and ChatSession factories can read it:
# macOS / Linux
export ZIWEI_API_KEY="sk_ziwei_..."
# PowerShell
$env:ZIWEI_API_KEY="sk_ziwei_..."
import { z } from 'zod';
import { run } from '@openai/agents';
import { iztroZiweiAgent, ChatSession, tool } from 'openai-iztro-agents';
const addToCalendar = tool({
name: 'add_to_calendar',
description: "Add an event to the user's calendar. Runs locally.",
parameters: z.object({ date: z.string(), title: z.string() }),
execute: async ({ date, title }) => `Added '${title}' on ${date}`,
});
const agent = iztroZiweiAgent({ tools: [addToCalendar], apiKey: 'sk_ziwei_...' });
const session = new ChatSession({ externalUserId: 'user_42' });
const result = await run(
agent,
'I was born 1990-06-15 at 10am, male. Pick a good day next week and add it to my calendar.',
{ session },
);
console.log(result.finalOutput);iztroZiweiAgent(...) returns a stock Agent whose model is the hosted Ziwei agent — so everything from the OpenAI Agents SDK works unchanged (result.newItems, streaming via run(agent, input, { stream: true }), handoffs, tracing, …).
Both hosted models use the same supported subset of the native OpenAI Agents SDK
modelSettings. Deep reasoning and sampling are separate modes:
import type { ModelSettings } from '@openai/agents';
import { iztroQimenAgent, iztroZiweiAgent } from 'openai-iztro-agents';
const deepSettings: ModelSettings = {
reasoning: { effort: 'high' as const },
maxTokens: 384000,
toolChoice: 'auto',
parallelToolCalls: true,
providerData: {
language: 'vi',
metadata: { current_datetime: '2026-07-20T14:30:00+08:00' },
},
};
const fastSettings: ModelSettings = {
reasoning: { effort: 'none' as const },
temperature: 0.4, // Or topP: 0.9; do not set both.
providerData: { language: 'vi' },
};
const ziwei = iztroZiweiAgent({ modelSettings: deepSettings });
const qimen = iztroQimenAgent({ modelSettings: fastSettings });Omit reasoning, or use none, minimal, or low, for the faster non-thinking path. medium, high, and xhigh use the same high deep-reasoning path. temperature and topP work only on the non-thinking path and are mutually exclusive. DeepSeek does not support frequencyPenalty or presencePenalty; the hosted API rejects them instead of silently ignoring them. Omit maxTokens to keep the current 384,000-token default output capacity.
Non-thinking mode prioritizes speed and can miss or mis-associate details in a complex chart. Use reasoning: { effort: 'high' } for cross-palace Ziwei synthesis, multiple fortune layers, or Qimen decisions that combine chart and timing evidence. Deep reasoning is generally more reliable for these readings, but it does not guarantee correctness.
Supported response languages are zh, en, ko, ja, and vi; setting providerData.language keeps all user-facing prose and translated terminology in that language without mixing. For live Qimen requests, create providerData.metadata.current_datetime from the user's local clock on each turn rather than keeping the fixed example value.
See the complete Model settings support matrix, including raw HTTP names and upstream SDK fields that are not hosted-model controls.
iztro-qimen-v3 is a hosted Qimen Dunjia model for a time-sensitive decision about one concrete matter. Use it for questions such as:
- Should we advance this partnership now, negotiate first, or pause?
- How is this interview, offer, launch, trip, or relationship decision likely to develop?
- If the matter can move forward, which dates are the meaningful action windows?
It casts the chart from the question time, so it does not need a birth date, birth hour, or gender. Use iztro-ziwei-v3 instead for natal personality, compatibility, or long-range life and fortune analysis.
| Model | Best for | Required input |
|---|---|---|
iztro-qimen-v3 |
One current event, decision, outcome, and optional timing | The concrete situation and question time |
iztro-ziwei-v3 |
Natal profile, compatibility, and longer-term fortune cycles | Birth date, birth time, and gender |
- Ask about one concrete matter and put unrelated decisions in separate runs.
- Give the current facts, choices, and constraint, then ask one explicit decision.
- When timing matters, ask for an action window and pass the user's local question time.
Timing results are candidate trigger windows to interpret with the complete answer, not guaranteed outcomes.
Use iztroQimenAgent(...) for a ready-to-run stock Agent, or iztroQimenModel(...) when constructing the Agent yourself:
import { run } from '@openai/agents';
import { iztroQimenAgent } from 'openai-iztro-agents';
const agent = iztroQimenAgent({
apiKey: process.env.ZIWEI_API_KEY,
// Optional: pin the user's local question time for reproducible charts.
// If omitted, the service uses the request time.
modelSettings: {
reasoning: { effort: 'high' },
providerData: {
metadata: { current_datetime: '2026-07-20T14:30:00+08:00' },
},
},
});
const result = await run(
agent,
'我们正在谈一项渠道合作,已经沟通两次,但分成和上线时间还没定。' +
'现在适合主动推进、继续谈判,还是暂缓?如果适合推进,请给出近期时间窗口和行动建议。',
);
console.log(result.finalOutput);For a strong request, describe the current situation, ask one decision, and say whether you need timing. Put unrelated matters in separate runs so each receives its own chart. See the complete 12-qimen-decision.ts example and compare both public models in the Models guide.
Public Iztro calculation names are available through Iztro tool events. Your own function tools, MCP servers, and human-in-the-loop continue to use the normal OpenAI Agents SDK interfaces.
The wrapper exposes the public calculation names returned by the API as Iztro tool events:
const result = await run(agent, '用奇门起局并判断应期。');
const event = result.rawResponses.at(-1)?.toolEvent;
console.log(event?.type, event?.tools); // tool_event ['qimen-qigua', 'qimen-yingqi']The complete list of public return values is in the Models guide. Do not depend on undocumented names or infer internal implementation from these values.
Streaming can include IztroToolEvent. The older
.iztroTools, lastIztroTools, and IztroToolsStreamEvent names still work for
compatibility, but new code should use toolEvent / IztroToolEvent.
History is stored on the server with a server-generated id, owned by your externalUserId:
import { ChatSession, listUserConversations } from 'openai-iztro-agents';
const session = new ChatSession({ externalUserId: 'user_42' }); // ZIWEI_API_KEY from env
await run(agent, 'My name is Alice.', { session });
await run(agent, "What's my name?", { session }); // remembers
const convId = session.sessionId; // save to resume later
new ChatSession({ conversationId: convId }); // resume
// Manage a user's chats:
await listUserConversations('user_42');sessionId precedence: explicit conversationId > a server-assigned id created lazily on first use. Reading sessionId before the conversation exists throws.
Fork a complete conversation, or copy only the first N SDK session items before continuing with replacement text:
const forked = await session.fork(); // copy the whole conversation
const edited = await session.fork({ itemCount: 4 }); // copy items 0..3, then branch
await run(agent, 'Use this edited question instead', { session: edited });The runnable ChatSession full-stack demo combines this with conversation lists, titles, deletion, history editing, live tool/chart indicators, Markdown rendering, and SSE streaming while keeping the API key on the Node backend.
Your tools use the SDK's native controls; the iztro tools are hidden:
const agent = iztroZiweiAgent({
tools: [...],
modelSettings: { toolChoice: 'auto', parallelToolCalls: true },
});const sendEmail = tool({
name: 'send_email', description: '…',
parameters: z.object({ to: z.string(), subject: z.string(), body: z.string() }),
needsApproval: true,
execute: async ({ to }) => `sent to ${to}`,
});
let result = await run(agent, '…');
while (result.interruptions.length) { // SDK pauses before the tool runs
for (const item of result.interruptions) {
result.state.approve(item); // or result.state.reject(item)
}
result = await run(agent, result.state);
}import { MCPServerStdio } from '@openai/agents';
const weather = new MCPServerStdio({ command: 'uvx', args: ['mcp-server-weather'] });
const agent = iztroZiweiAgent({ mcpServers: [weather], apiKey: KEY });| Export | Mirrors the Python | Notes |
|---|---|---|
iztroZiweiAgent(opts) |
iztro_ziwei_agent(...) |
stock Agent, hosted model |
iztroZiweiModel(opts) |
iztro_ziwei_model(...) |
stock OpenAIChatCompletionsModel |
iztroQimenAgent(opts) |
iztro_qimen_agent(...) |
stock Agent, hosted Qimen model |
iztroQimenModel(opts) |
iztro_qimen_model(...) |
stock OpenAIChatCompletionsModel |
ChatSession |
ChatSession |
server-side memory (Session) |
listUserConversations(id, opts) |
list_user_conversations(...) |
list a user's chats |
DEFAULT_BASE_URL, IZTRO_ZIWEI_MODEL, IZTRO_QIMEN_MODEL, TOOL_EVENT_TYPE |
same | constants |
re-exports: Agent, Runner, run, tool |
Agent, Runner, function_tool |
from @openai/agents |
Options use camelCase (apiKey, baseUrl, externalUserId, modelName) — the JS convention — where Python uses snake_case.
# Fast, deterministic, offline (no key) — stubs the model + conversation HTTP:
npm test # vitest; the live test self-skips
# Live end-to-end against a deployed backend (opt-in):
ZIWEI_API_KEY=sk_ziwei_... npx vitest run tests/live.test.ts
# defaults to dev; prod via ZIWEI_BASE_URL=https://chat-api.iztro.comThe offline suite covers a wide range of scenarios (each can graduate into an examples/ script):
| File | What it exercises |
|---|---|
tests/toolLoops.test.ts |
plain chat, single/parallel/sequential tool calls, typed args, local-tool errors, unicode, toolChoice/parallelToolCalls, and that iztro tools stay hidden |
tests/humanInTheLoop.test.ts |
native needsApproval — approve, reject, mixed approve+reject |
tests/streaming.test.ts |
streamed text deltas reassembling into finalOutput |
tests/session.test.ts |
ChatSession memory — lazy id, add/get/pop/clear, multi-turn, ownership + listing, resume |
tests/factories.test.ts |
credential/base-url resolution, /v2 suffix, SDK arg passthrough |
Shared offline backends live in tests/_mock.ts (a stubbed chat-completions endpoint and an in-memory conversation store).
- Birth details are gathered by the Ziwei agent through the conversation — there is no
birthInfoparameter. - The backend currently streams an answer as a single chunk (not token-by-token); streaming works but token-level streaming is a future backend enhancement.
- Streaming together with developer tools is not yet supported — use non-streaming
runfor tool loops. - Multi-turn tool loops re-send the prompt each round, so they cost more tokens.