Eposyne Director

The decide request

The complete model of POST /v2/decide: the JSON request, the JSON response, headers, media rules, and limits. The format is director/v2.

The call

Request line and headers
POST https://director.eposyne.com/v2/decide
Authorization: Bearer epd_...
Content-Type: application/json

The body is one JSON object, the decision request. Its first member, schema, names the format: eposyne.director.decision-request/v2. Director takes application/json and nothing else, and answers application/json; charset=utf-8 on success and on error. There is no XML form. See Media rules.

Request headers

HeaderRequiredMeaning
AuthorizationYesBearer followed by your Director API key.
Content-TypeYesapplication/json. A charset parameter is allowed and must be utf-8.
AcceptNoLeave it out, or make sure one entry admits application/json (*/* and application/* do). Otherwise the call is a 406.
X-Eposyne-PluginNoA label for your client in the form name/version, for example my-game-server/1.4.0. It appears in logs and usage records. It never changes how Director behaves.

Query parameters

ParameterMeaning
provider and modelChoose the model for this request. Send both or neither. See Models and usage.
max_tokensThe output-token cap for this call. See Models and usage.
rationaletrue to get a short reason with every change. See Reasons per change.
world, timeline, and after_seqThe stream position the decision follows. Send all three or none. See below.
player_idWhose Plots verdicts the reply carries. Only with a stream position, else 422. Without it, the player of the event at after_seq. See The Plots verdict.

The stream position parameters tie a decision to the events you sent to POST /v2/events. They are not needed for plain decisions. With a position, Director first processes every event up to after_seq (409 if it cannot within 10 seconds, with nothing charged), then lets Plots and Memory take part in the decision. The reply then carries a plots member when Plots is on. See The Plots verdict and Memory in a decision.

The request document

JSON
{
  "schema": "eposyne.director.decision-request/v2",
  "ask": "Should the guards pursue the fleeing thief or hold position to protect the dignitary?",
  "variables": {
    "guard_stance": "patrol",
    "guard_morale": 50,
    "thief_distance": "50m",
    "dignitary_threat": "low",
    "reinforcements_available": true
  },
  "mutable": [
    {"name": "guard_stance", "allowed_values": ["patrol", "pursue", "hold", "alert"]},
    {"name": "guard_morale", "range": {"min": 0, "max": 100}},
    {"name": "dignitary_threat", "allowed_values": ["low", "medium", "high"]}
  ]
}

The members

MemberRequiredMeaning
schemaYesExactly eposyne.director.decision-request/v2.
askYesThe question, as plain text. One decision per request works best. Angle brackets and every other character are ordinary text.
variablesYesThe read-only snapshot of game state: an object whose keys are variable names and whose values are JSON scalars (string, number, boolean, or null). Up to 1,024 entries.
mutableYesThe variables the model may suggest a change for. An array of entries, each with a name and exactly one rule: allowed_values, free_text, range, or length. Up to 1,024 entries.
operationsNoThe engine functions the model may propose calling. See Operations and calls.
validationNotrue turns on a second check for generated text. See Validation.
metadataNoAn object of up to 64 scalars that Director records with the trace and echoes in the response. The model never sees it.

A member not in this table is refused with 422 schema_violation. Variable names start with a letter or underscore and are at most 128 characters of letters, digits, _, ., : and -.

Values keep their type

A value in variables is sent with the JSON type you mean: 50 is a number, "50" is a string, true is a boolean. Director never converts. The rule you declare for a mutable variable must fit the type of its current value: a range on a variable whose value is the string "50" is refused with 422 value_type, and the variable member of the error names it. A proposed change keeps the type too, so your engine can apply it without parsing.

The rules

RuleMeaning
allowed_valuesA closed list of strings, numbers, or booleans. The proposed value must equal one entry, type included: "1" does not match 1, and Hold does not match hold.
free_texttrue. Any string is allowed. Use it for generated content such as dialogue. The model is asked to always fill in a free-text variable; for other variables it changes a value only when a change makes sense.
range{"min": 0, "max": 100}, both required, inclusive. The proposed value must be a finite number within the bounds.
length{"min": 1, "max": 4}, both required, inclusive. The proposed value must be a string whose length in characters is within the bounds.

Each entry has exactly one rule. An entry with two rules, or none, is refused with 422 schema_violation. A name that appears twice under mutable is refused the same way. A mutable name that is absent from variables is allowed.

A free-text line and a closed list
{
  "schema": "eposyne.director.decision-request/v2",
  "ask": "The player asks Mara where her son went. What does Mara say?",
  "variables": {"npc_line": "", "mood": "sad"},
  "mutable": [
    {"name": "npc_line", "free_text": true},
    {"name": "mood", "allowed_values": ["sad", "hopeful", "angry"]}
  ],
  "validation": true
}

Operations and calls

When the model may also propose an engine function, declare the functions under operations. The reply then carries a calls array with at most limit entries (0 or 1, default 1), each naming a declared operation and supplying every declared parameter with a value of its type, within its allowed_values or range. A call that names an undeclared operation, misses an argument, adds one, or has a wrong type is dropped, never passed through. request_id is yours: use it to apply a proposal only to the request it was made for.

Request with operations
{
  "schema": "eposyne.director.decision-request/v2",
  "ask": "The player offers the guard a bribe of ten coins. What does the guard do?",
  "variables": {"guard_line": "", "guard_mood": "wary", "coins_offered": 10},
  "mutable": [
    {"name": "guard_line", "free_text": true},
    {"name": "guard_mood", "allowed_values": ["wary", "greedy", "angry"]}
  ],
  "operations": {
    "request_id": "req-7f3a",
    "limit": 1,
    "available": [
      {"id": "Guard.OpenGate", "description": "Open the town gate for the player.", "parameters": []},
      {"id": "Guard.CallReinforcements", "description": "Summon more guards.",
       "parameters": [{"name": "count", "type": "integer", "range": {"min": 1, "max": 4}}]}
    ]
  }
}
Response with a call
{
  "schema": "eposyne.director.decision-response/v2",
  "changes": [
    {"name": "guard_line", "value": "Ten coins buys you nothing but my silence. For now."},
    {"name": "guard_mood", "value": "greedy"}
  ],
  "calls": [
    {"id": "Guard.CallReinforcements", "arguments": {"count": 2}}
  ]
}

A parameter has a name, a type (string, number, integer, or boolean), and optionally a description, allowed_values, or range. Up to 256 operations with up to 64 parameters each.

Validation

A member of the request
"validation": true

With validation on, Director makes a second model call that reviews generated text for promises the character cannot keep, for example an NPC who offers a free item when no variable allows it. The corrected text replaces the original. The pass only runs when the reply contains longer text, it costs an extra model call, and it roughly doubles latency. Use it for player-facing dialogue. Skip it for enum and number decisions. validation overrides the deployment's default for this one request.

Metadata

Request with metadata
{
  "schema": "eposyne.director.decision-request/v2",
  "ask": "Does the merchant lower the price?",
  "variables": {"price": 120},
  "mutable": [{"name": "price", "range": {"min": 80, "max": 120}}],
  "metadata": {"scene": "market-02", "attempt": 1}
}

The response carries the same object back as metadata. Use it to correlate a reply with the scene or the frame that asked.

Reasons per change

Add rationale=true to the URL to get a short reason with every change, for example /v2/decide?rationale=true. Without it, or with rationale=false, a change has only name and value, which saves output tokens and time on every call. The reason is written after the value, so asking for it does not change the decision. Ask for it while you develop and leave it out in a shipped game. Any value other than true or false, or the parameter twice, is a 400 with "code": "invalid_rationale".

The response document

This example was made with rationale=true; without it the rationale members are absent.

JSON
{
  "schema": "eposyne.director.decision-response/v2",
  "changes": [
    {
      "name": "guard_stance",
      "value": "hold",
      "rationale": "Dignitary protection takes priority over one thief."
    }
  ]
}
MemberMeaning
schemaExactly eposyne.director.decision-response/v2.
changesAlways present. Zero or more entries. Empty means no suggestions.
changes[].nameOne of your mutable variables. Never anything else.
changes[].valueThe suggested value, with its JSON type. It satisfies the rule you declared.
changes[].rationaleOnly with rationale=true. A short explanation, up to 200 characters. Useful for logs and design review. Do not parse it.
callsOnly when the request declared operations. At most limit entries, each an id and an arguments object.
plotsOnly for a decision at a stream position with Plots on: what Plots did for the player. See Plots.
metadataOnly when the request sent it. The same object, echoed.

There is at most one change per variable. Everything else the model wrote is dropped before the reply reaches you. A response with no suggestions looks like this:

JSON
{"schema": "eposyne.director.decision-response/v2", "changes": []}

A model output that is not one JSON object of the expected shape gives the same empty reply, with the call charged. Director repairs nothing and makes no second attempt.

Ignore members and headers you do not recognize. We may add them within /v2.

Response headers

A successful decision carries these headers. The token and cost headers appear only when the model backend reported usage, so a missing one is not an error.

HeaderMeaning
X-LLM-ProviderThe provider that answered.
X-LLM-ModelThe model that answered.
X-LLM-Prompt-TokensInput tokens used.
X-LLM-Completion-TokensOutput tokens used.
X-LLM-Total-TokensInput plus output tokens.
X-LLM-Cost-UsdCost as a decimal string with six fractional digits, in US dollars. With validation on, the figures cover both model calls.
X-LLM-Max-TokensThe output-token cap the call ran with. Set it with max_tokens; see Models and usage.
X-LLM-Finish-Reasonstop, length, content_filter, or other. length with no change means the answer was cut off at the cap.

Your credits are in euro. To see your spend, read GET /v2/usage/me instead of converting the dollar header.

Media rules

The codes and their bodies are on Errors and retries.

Limits

LimitValueWhen exceeded
Request body262,144 bytes413 body_too_large
Prompt size30,000 input tokens413 prompt_too_large
JSON nesting depth32 levels422 nesting_too_deep
Variables and mutable entries1,024 each422 schema_violation
Rationale length200 charactersCut to 200
Model call time30 seconds502
Request rate60 per minute per account429

Set your client timeout to at least 35 seconds, and to 70 seconds when validation is on. Fire the request before the player reaches the decision point, and fall back to scripted behavior while you wait.