Skip to content

Jev ​

Jev is TypeSafe's “System One” decision model: instead of text it returns calibrated probabilities for structured questions. The remote decision API can instead be the OpenAI Decisions API (jev.provider: "openai", model gpt-6-luna): the questions are identical, yes/no questions go out as predicate and choices as choice, a yes/no question's criteria are folded into its instructions, and a refusal counts as a failed call. Jingmei shares one decision client across participation decisions, final-text audits, reactions, memory ranking, and optional event assignment and participation scoring.

Participation decisions (replyDecision, default true). Explicit mentions, replies and name/alias matches route directly without a participation request. Every other human message gets one request when enabled and a client exists: given the speaker, choose among all scoped characters and none. Without explicit addressing, only an obvious response to what the character just said counts (the character having just spoken does not), and uncertain cases choose none. A selected character with winning probability ≥ 0.5 routes directly, bypassing sampling and gates, and counts as addressed for quick reactions.

  • Ordinary candidates still come from cumulative routingP HMAC sampling. Any message from the character's current-platform account in the last 30 seconds puts it on cooldown. It also stays silent when, among at most 30 latest messages in the last 10 minutes, its account has at least 3 messages, there are more than 1 distinct other authors, and its share is ≥ 25%.
  • Only a sampled, ungated candidate adds the chat_in noul scoring question to that same request; it needs a score ≥ replyThreshold (default 0.7). Directed-content detection still runs when there is no candidate or a gate blocks it. Failure logs participation_failed with an error category and routes nobody. Disabled or without a client, routing uses only HMAC sampling and gates.

Final-text audit. Before sending final text (or converting it to explicitly requested voice), a deterministic check rejects §E followed by a digit or [当前事件; no audit request is made for these internal-marker leaks. With a client, a noul naturalness score < 0.5 also rejects the reply; a recap the user explicitly asked for (a summary or timeline) counts as natural.

  • One rewrite: a turn's first rejection does not go silent. For a failed naturalness score the decision model is asked once more for the reason (recapping the chat, planning, listing drafts, other), and the character gets a concrete hint for that reason (a marker leak gets a hint to drop the markers) and rewrites; reply_rewrite is logged. Final text learns it through a hidden jingmei_audit_gate_v1 message that triggers the rewrite; send_reply learns it from its tool result and the turn stays open, so the character can call it again or answer in text. The rewrite is audited as usual.
  • Rejected again: nothing of the reply is sent; instead the fixed line (系统提示:说了不该说的东西被捂嘴了) goes out (replying to the message, stored), with text-free reply_withheld; final text also persists a hidden jingmei_withheld_v1 marker. Later provider projections drop the rejected draft, the rewrite hint and the withheld reply so unsent text cannot become chat history.
  • Failed audit call: content-directed and probability replies are withheld silently without a rewrite (audit_failed; nothing was judged, so no notice), while explicit mention/reply/name routes are allowed. Without a client, only the marker check runs.

send_reply runs the same audit before preparing any part: all its text parts and voice transcripts are scored together, and captions and text-image Markdown also get the marker check. Media-only replies (pictures, reaction images) skip the naturalness score.

Quick reactions (quickReactions). Every human message with text gets one Jev request asking three things at once: pick an emoji from the table (or none), is the message strongly emotional, is it funny.

  • Addressed messages (mention, reply, name, or a content-directed character): the corresponding character adds the chosen emoji, nothing if Jev chose none.
  • Ordinary messages: an emoji is added only if max(strong emotion, funny) ≥ threshold and the channel's previous such reaction was at least minIntervalMs ago, by the first character configured for that group. Messages inside the rate-limit window don't call Jev at all.
  • Runs alongside the main reply; failures are only logged and never affect the reply. While enabled, the main model's react_to_message tool is not registered — reactions belong to Jev.

Memory ranking (memoryScoring). When a character explicitly calls recall_member_memory, candidate facts and relationships (per member: the 20 newest facts and 16 strongest relationships) are scored against the current message in a single Jev request, keeping the 5 most relevant facts and 4 relationships per member. If Jev is unavailable it falls back to recency and interaction count.

Local wrapper and fallback. When jev.apiKeyEnv resolves, requests go to jev.endpoint first. If a local LLM is configured, any remote error retries once through the in-process notjev wrapper. Without a remote key, the wrapper is used directly; no extra HTTP server is started. “Local” describes the wrapper, not necessarily its LLM: absent localJev plus a resolved DEEPSEEK_API_KEY and no remote key defaults to https://api.deepseek.com / deepseek-flash. With a remote key that default does not apply, so there is no fallback unless localJev is written out. An explicit localJev replaces that default completely; omitting its apiKeyEnv sends no authentication.

The wrapper has a 30-second default timeout and disables DeepSeek thinking (thinking.type=disabled). Its LLM must return logprobs; missing logprobs become an invalid_response call failure. When the model abstains, the wrapper takes the highest-probability option (argmax).

Quick reactions and memory ranking still require an explicit jev section; events alone or a DeepSeek key alone does not enable them. Participation decisions default to enabled whenever a shared decision client exists; final-text audits also run with a client regardless of replyDecision. You may omit jev.apiKeyEnv to use only the wrapper, but then a local LLM must exist (a localJev section or DEEPSEEK_API_KEY); otherwise it is a configuration error. Without any jev section, routing falls back to deterministic sampling/gates and marker checks. An explicitly named apiKeyEnv missing from .env / the process environment is still a configuration error, not a silent fallback. Wrapper calls are billed by the chosen LLM provider.

Cost. Remote Jev bills input tokens only; output is free (jev-1.13 was $0.042 per million tokens at the time of writing — see the official pricing). A reaction request carries just the message, up to 5 recent chat lines (each cut to 200 characters) and three questions — typically a few hundred tokens — and the remote request times out after 3 seconds. It uses none of the main model's tokens; the local wrapper consumes tokens from its configured LLM.

With participation enabled and a client available, each human message without an explicit addressee adds +1 decision request (directed detection plus optional participation score combined), and each final-text reply adds +1 naturalness audit (a rejection adds +1 reason request and the rewrite +1 audit). Explicit addressing skips participation; marker leaks skip the audit. These calls do not use the main reply model, but the local wrapper incurs its LLM's cost; remote fallback can add one local LLM call.

Configuration. Put TYPESAFE_API_KEY: … in .env and add to jingmei.config.json (for OpenAI use "provider": "openai" and "apiKeyEnv": "OPENAI_API_KEY", and omit endpoint and model to get its defaults):

json
"jev": {
	"endpoint": "https://api.typesafe.ai/v1/systemone",
	"apiKeyEnv": "TYPESAFE_API_KEY",
	"model": "jev-latest",
	"quickReactions": true,
	"memoryScoring": true,
	"replyDecision": true,
	"replyThreshold": 0.7,
	"threshold": 0.8,
	"minIntervalMs": 60000
}
FieldDefaultMeaning
providertypesafeRemote wire format: typesafe or openai
endpointhttps://api.typesafe.ai/v1/systemoneRemote Jev http(s) URL; openai defaults to https://api.openai.com/v1/decisions
apiKeyEnvnoneEnv var for the remote key; omit to use only the local wrapper
modeljev-latestCan be pinned, e.g. jev-1.13.0; openai defaults to gpt-6-luna
quickReactionstrueQuick reactions
memoryScoringtrueMemory ranking
replyDecisiontrueContent-aware participation; disabling it retains deterministic candidate gates and final-text audits
replyThreshold0.7In (0, 1]; minimum score for an ordinary sampled candidate to chime in
threshold0.8In (0, 1]; minimum score for reacting to an ordinary message
minIntervalMs60000Minimum gap between two ordinary-message reactions in one channel
emojisplatform defaultsPer-platform override: { "discord": { "👍": "agree" }, "telegram": { … } }; the key none is reserved

Default tables (emoji → meaning given to Jev, in Chinese in the code):

MeaningDiscordTelegram
Agree, got it👍👍
Funny😂🤣
Sad, crushed😭😭
Heartwarming, thanks❤️❤
Puzzled, unsure🤔🤔

Emojis in a custom Telegram table that the Bot API does not allow are dropped with a warning.

Topic configuration ​

json
"events": {
	"summaryModel": "deepseek/deepseek-flash",
	"embeddingModel": "fast-bge-small-zh-v1.5"
}

A reply to a message that already has an event inherits that event without a decision call (bot replies too; bot messages that reply to nothing have no event). A human message with fewer than 2 letters/digits after removing bare media placeholders such as [贴纸 …], [视频 N帧] and [文件] joins the most recently active topic from the last 10 minutes. Other human messages go through the decision client whenever topic candidates exist: short replies, follow-ups and emotional reactions lean towards the ongoing topic, and a “new” decision whose probability is below 0.6 is reassigned to the most probable existing topic. Topics with a message in the last 2 hours are active. Candidates are up to 5 recent active topics, 2 older topics recalled by vector similarity within the same space/channel, and “new”. At 3, 6, 12, 24… messages, a background single-flight refresh generates the title/description, scores participation and updates the embedding without blocking channel processing. Summaries use the independent summaryModel; the event note is attached only to the triggering input for that turn, dropped from history from the next turn on, and never enters the system prompt.

Topic summaries are instructed to neutrally describe what is being discussed or played: a running joke stays a joke, without judging members' behavior, assigning moderation tasks to the character, or retaining private member details unrelated to the topic.

BSD 2-Clause License