Use your model in Godot
Your downloaded model can decide inside your Godot 4 game: from GDScript, offline, every frame if you like. It makes exactly the same decisions as the hosted /v1/decide and the Python runtime, in the same response format, with your rules enforced on every decision.
No Python and no ONNX Runtime: the addon reads the model file itself. The addon is a 33 KB download; a Snake model is 175 KB and a Doom model about 800 KB. It works on every platform Godot exports to, web included (Godot 4.3 or later).
Install
Download canonopy-godot-0.1.1.zip and unzip it at the root of your project (it creates addons/canonopy/). Then turn it on in Project > Project Settings > Plugins > Canonopy, so your model files go into exported games.
Want to see it first? canonopy-godot-example-0.1.1.zip is a whole Godot project: Snake, played by a model. Open it and press Play.
Two steps
1. Put your model in the project. Download it and drop the zip anywhere under your project, for example res://models/snake@1.zip. Don't unzip it.
canonopy download snake@12. Ask it, from GDScript.
var model := CanonopyModel.load("res://models/snake@1.zip", {"best_guess": true})
func _physics_process(_delta):
var res := model.decide({"state": {
"snakeLength": 12,
"moves": {
"up": {"eats": false, "food_distance": 4, "room": 180, "dead_end": false, "tail_reachable": true},
"right": {"eats": true, "food_distance": 0, "room": 175, "dead_end": false, "tail_reachable": true},
},
}})
var move = res["decision"]["action"] # "right"The state is the same JSON your model was set up with, as a Dictionary. CanonopyModel.load returns null when it can't load the model, and CanonopyModel.last_error says why.
What comes back
The same response as the hosted API: answers per question, and decision with your rules applied (action, confidence, blocked_by_rules, blocked_actions, route).
routeis"act"at or above the model's safety bar,"review"below it, and"ask"when your rules left no action. With best guess on (below), an answer below the bar says"act"too, plus"best_guess": true.{"questions": {...}}in the request asks only some questions, or some of a question's options.- On a problem the response is
{"error": {"type", "message"}}. model.decide_many(states)decides many states at once.
Best guess (for games)
For games, turn on best guess: the model always acts, and tells you how sure it was.
var model := CanonopyModel.load("res://models/snake@1.zip", {"best_guess": true})
var res := model.decide({"state": state})
res["decision"]["action"] # always a move your rules allow
res["decision"]["confidence"] # how sure it was
res["decision"].get("best_guess", false) # true when it was unsure and acted anywayIn a game, "unsure" usually means two moves are about as good, not a real doubt. With best guess on, the model plays its most likely move instead of routing it, and marks it: "route": "act" plus "best_guess": true, with the confidence and probabilities as usual. A confident answer has no best_guess field, so you can always tell them apart. Your rules still apply first: a blocked move is never the guess, and when your rules allow no move, action is null and route is "ask". A best guess doesn't call your fallback or the hosted service.
It is off by default, because business decisions should keep routing unsure cases to a person. model.decide(request, Callable(), true) (or false) sets it for one call. The example project has it on and shows "model", "best guess" or "rule" for every move.
Your own fallback
With best guess off, you can answer the moves the model isn't sure of yourself:
func my_fallback(state, questions): # only the unsure questions
return {"move": safest_move(state)} # an option is enough; null leaves it held for review
var res := model.decide({"state": state}, my_fallback)Its answers say "source": "local_fallback", and your rules still apply after them.
Models that read text
A model that reads messages needs the text reader (34 MB and up), too big to ship in a game. Those models decide through the hosted service: add a CanonopyHosted node to your scene, then
var res = await model.decide_async({"state": {"message": "Where is my order?"}}, $CanonopyHosted)For numbers models, decide_async answers locally when the model is sure, and sends only the unsure answers to the service (with best guess on, it acts on them locally instead). If the service can't be reached, they go to your fallback, else they are held for review.
Never ship your API key inside a game
Players can read anything inside a released game, a key in a script or a scene included, and anyone with your key can make decisions on your account. While you develop, set CANONOPY_API_KEY in your environment before opening Godot: CanonopyHosted reads it, and nothing goes into your project. In a released game, point CanonopyHosted's base_url at your own server and leave its key empty: your server keeps the key, adds Authorization: Bearer <key>, and forwards POST /v1/decide (same JSON body) to https://api.canonopylabs.com/v1/decide.
Faster decisions
Everything runs in GDScript. For big models deciding every frame, an optional native library runs the model about 50 times faster: unzip canonopy-godot-native-0.1.1-macos.zip at the root of your project and restart Godot. Same decisions either way; model.uses_native() says which one runs. macOS is built (Apple silicon and Intel); for Windows and Linux, build it from the addon's source (one C++ file, no dependencies). The web uses GDScript.
One decide() call (reading the state, the model, your rules and the answer), median over 300 recorded game states on a laptop:
| Model | GDScript | With the native library |
|---|---|---|
| Snake (21 numbers, 1 question) | 0.64 ms | 0.14 ms |
| Doom (61 fields, 4 questions) | 2.3 ms | 0.50 ms |
Same decisions as everywhere else
Every release is checked against the Python runtime on real models (Snake, Doom, and a numbers-and-categories model with every kind of question and rules on other answers): 2,000 varied requests each, with missing fields, numbers written as text, unknown categories and edge values, with best guess off and on. Chosen answers, routes, best guess flags, rule outcomes and refusals were identical in every case.
Not yet
- Reading text on the device (text models decide through the hosted service).
- Saving unsure cases on the device to sync later (the Python runtime's
sync). - Models downloaded before Sep 26, 2026: download them again.