@novedu/cli 0.23.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -12
- package/dist/main.js +197 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,7 +85,8 @@ npx @novedu/cli prompts ./sorting-quiz.yaml --kind quiz --json \
|
|
|
85
85
|
re-implementation), so what you see is what the model gets: fragments resolved,
|
|
86
86
|
and — for a compound quiz — every `quiz_files` include fetched, each imported
|
|
87
87
|
question carrying its source quiz's preamble.
|
|
88
|
-
- Every dump carries `{ kind, id, llm: { provider, model } }
|
|
88
|
+
- Every dump carries `{ kind, id, llm: { provider, model, reasoning? } }` (the
|
|
89
|
+
reasoning level only when the file sets one). A **quiz** adds
|
|
89
90
|
`grading` (a `system` prompt per question, the user-message templates and the
|
|
90
91
|
grader's JSON-Schema response contract) and `discussion` (the chat's `system`
|
|
91
92
|
prompt, the three seed-message templates and the verdict wording). A **coding**
|
|
@@ -149,6 +150,9 @@ npx @novedu/cli eval ./sorting-quiz.eval.yaml --repeats 3
|
|
|
149
150
|
npx @novedu/cli eval ./sorting-quiz.eval.yaml \
|
|
150
151
|
--llm-provider "Azure Foundry" --llm-model gpt-5-mini
|
|
151
152
|
|
|
153
|
+
# Same model, more thinking: the level alone keeps the activity's provider/model
|
|
154
|
+
npx @novedu/cli eval ./sorting-quiz.eval.yaml --llm-reasoning high
|
|
155
|
+
|
|
152
156
|
# A strong judge over the quiz's own grader — the recommended pairing
|
|
153
157
|
npx @novedu/cli eval ./sorting-quiz.eval.yaml \
|
|
154
158
|
--judge-llm-provider "Azure Foundry" --judge-llm-model gpt-5.6-terra
|
|
@@ -225,12 +229,20 @@ npx @novedu/cli eval ./loops-tutor.eval.yaml --report loops.md
|
|
|
225
229
|
only when some case required a tool, so no line means "not checked"), a **"Missing tool
|
|
226
230
|
calls"** section in the Markdown report, and `totals.toolsFlagged` plus each repeat's
|
|
227
231
|
`toolCalls` / `missingTools` in the JSON.
|
|
228
|
-
- **Choosing
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
232
|
+
- **Choosing what runs.** `--llm-provider` + `--llm-model` (both or neither) replace the
|
|
233
|
+
activity's **whole** `llm:` block for the run — its reasoning level included, so the
|
|
234
|
+
file's level is dropped unless `--llm-reasoning <level>` restates it.
|
|
235
|
+
`--llm-reasoning` on its own changes only the effort and keeps the file's
|
|
236
|
+
provider/model — the "same model, more thinking" comparison run.
|
|
237
|
+
- **Choosing the judge.** By default the judge runs on the same model **and effort** as
|
|
238
|
+
the grader. `--judge-llm-provider` + `--judge-llm-model` (both or neither) point it at
|
|
239
|
+
another one — replacing the whole spec, exactly like the grading flags — which is the
|
|
240
|
+
**recommended** setup: a strong judge over a smaller grader finds real
|
|
241
|
+
problems, while a small model judging itself mostly produces noise.
|
|
242
|
+
`--judge-llm-reasoning` sets the judge's effort on its own, no pair needed.
|
|
243
|
+
`--no-judge-feedback` turns judging off and halves the LLM calls; combining it with any
|
|
244
|
+
of the judge flags is rejected as contradictory. Because judging roughly doubles the
|
|
245
|
+
cost, the run's scope line says so
|
|
234
246
|
up front: `27 case(s) × 3 repeat(s) = 81 grading + 81 judge call(s)`.
|
|
235
247
|
- **If the judge itself fails**, the run **degrades instead of aborting**: after three
|
|
236
248
|
consecutive judge failures it stops judging (one warning on stderr) and finishes the
|
|
@@ -255,7 +267,7 @@ npx @novedu/cli eval ./loops-tutor.eval.yaml --report loops.md
|
|
|
255
267
|
run localises the problem for the cost of a single grading call.
|
|
256
268
|
- **Caveat**: a green run certifies **the file you ran it on**, not the app-hosted
|
|
257
269
|
copy a live code serves — upload it (`files upload`) afterwards. An override run
|
|
258
|
-
certifies the override
|
|
270
|
+
certifies the override, not the quiz's configured `llm`.
|
|
259
271
|
|
|
260
272
|
## Authentication
|
|
261
273
|
|
|
@@ -300,7 +312,7 @@ authoritative — the CLI sends your input as-is and relays the server's answer.
|
|
|
300
312
|
```
|
|
301
313
|
codes create --module <tutor|quiz|writing|coding> --file <url>
|
|
302
314
|
[--start <iso>] [--end <iso>] [--note <text>]
|
|
303
|
-
[--llm-provider <p> --llm-model <m>]
|
|
315
|
+
[--llm-provider <p> --llm-model <m>] [--llm-reasoning <level>]
|
|
304
316
|
codes list [--search <q>] [--module <m>] [--all]
|
|
305
317
|
codes sync <registry-file> [--lock <path>] [--dry-run] [--json]
|
|
306
318
|
files upload <name> [--kind <tutor|fragment|quiz|writing|coding>]
|
|
@@ -318,7 +330,10 @@ images list [--search <q>] [--all]
|
|
|
318
330
|
server-side before the code is stored, and the response includes the
|
|
319
331
|
shareable `url`. `--start`/`--end` must be ISO 8601 **with an explicit
|
|
320
332
|
offset or `Z`** (e.g. `2026-07-07T08:00:00Z`); the
|
|
321
|
-
`--llm-provider`/`--llm-model` override pair is both-or-nothing
|
|
333
|
+
`--llm-provider`/`--llm-model` override pair is both-or-nothing, and
|
|
334
|
+
`--llm-reasoning <level>` (`minimal`, `low`, `medium` or `high`) rides on top of
|
|
335
|
+
the pair — it is rejected without it. The override replaces the activity's whole
|
|
336
|
+
`llm:` block, so leaving the level out also drops the file's.
|
|
322
337
|
- `codes sync <registry-file>` mints codes for a whole **course** at once — see
|
|
323
338
|
[Many activities at once](#many-activities-at-once-codes-sync) below.
|
|
324
339
|
- `files upload <name>` is an **upsert**: creating a new file requires
|
|
@@ -416,11 +431,13 @@ activity-codes:
|
|
|
416
431
|
- **Groups decide the module:** `quizzes`, `tutors`, `writing`, `coding`. Each
|
|
417
432
|
entry gives either `file` (relative to `base-url`, which must end in `/`) or
|
|
418
433
|
an absolute `url`, plus any of `start`/`end` (ISO 8601 **with an offset or
|
|
419
|
-
`Z`**, whole seconds), `note`, and an `llm: {provider, model}`
|
|
434
|
+
`Z`**, whole seconds), `note`, and an `llm: {provider, model, reasoning?}`
|
|
435
|
+
override.
|
|
420
436
|
- **Keys are yours and must be unique across all groups** — lowercase letters,
|
|
421
437
|
digits and hyphens. Your material references the key; the lock file maps it to
|
|
422
438
|
the code.
|
|
423
|
-
- **Re-runs are safe.** An entry whose activity, window and
|
|
439
|
+
- **Re-runs are safe.** An entry whose activity, window and LLM override
|
|
440
|
+
(provider, model and reasoning level) match
|
|
424
441
|
an existing code of yours **reuses** that code; only entries without a match
|
|
425
442
|
are minted. So `codes sync` after every edit is the normal workflow, and the
|
|
426
443
|
first run against already-minted codes should report all-reused.
|
package/dist/main.js
CHANGED
|
@@ -85,17 +85,46 @@ async function acquireSilent(pca) {
|
|
|
85
85
|
return null;
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* How the system browser is launched per platform. Exported for tests.
|
|
90
|
+
*
|
|
91
|
+
* Windows goes through `cmd /c start`, and cmd RE-PARSES the command line it
|
|
92
|
+
* receives: an unquoted URL is cut at the first `&`, so Entra only ever saw
|
|
93
|
+
* `authorize?client_id=…` and answered AADSTS900144 ("the request body must
|
|
94
|
+
* contain the following parameter: 'scope'"). Node quotes an argument only
|
|
95
|
+
* when it contains whitespace, so the URL is quoted here explicitly and the
|
|
96
|
+
* command line handed over verbatim. The empty `""` is `start`'s window title
|
|
97
|
+
* — without it, `start` would take the quoted URL as the title.
|
|
98
|
+
*/
|
|
99
|
+
function browserCommand(url, platform = process.platform) {
|
|
100
|
+
if (platform === "darwin") return {
|
|
101
|
+
command: "open",
|
|
102
|
+
args: [url],
|
|
103
|
+
verbatim: false
|
|
104
|
+
};
|
|
105
|
+
if (platform === "win32") return {
|
|
106
|
+
command: "cmd",
|
|
107
|
+
args: [
|
|
108
|
+
"/c",
|
|
109
|
+
"start",
|
|
110
|
+
"\"\"",
|
|
111
|
+
`"${url}"`
|
|
112
|
+
],
|
|
113
|
+
verbatim: true
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
command: "xdg-open",
|
|
117
|
+
args: [url],
|
|
118
|
+
verbatim: false
|
|
119
|
+
};
|
|
120
|
+
}
|
|
88
121
|
function defaultOpenBrowser(url) {
|
|
89
|
-
const
|
|
90
|
-
"/c",
|
|
91
|
-
"start",
|
|
92
|
-
"",
|
|
93
|
-
url
|
|
94
|
-
]] : ["xdg-open", [url]];
|
|
122
|
+
const { command, args, verbatim } = browserCommand(url);
|
|
95
123
|
try {
|
|
96
124
|
spawn(command, args, {
|
|
97
125
|
stdio: "ignore",
|
|
98
|
-
detached: true
|
|
126
|
+
detached: true,
|
|
127
|
+
windowsVerbatimArguments: verbatim
|
|
99
128
|
}).unref();
|
|
100
129
|
} catch {}
|
|
101
130
|
}
|
|
@@ -284,9 +313,19 @@ async function runApiRequest(options) {
|
|
|
284
313
|
const LLM_PROVIDERS = ["SCCH", "Azure Foundry"];
|
|
285
314
|
const DEFAULT_PROVIDER = "SCCH";
|
|
286
315
|
const providerSchema = z.enum(LLM_PROVIDERS).default(DEFAULT_PROVIDER).meta({ description: "The LLM provider serving the model. For Azure Foundry, model is the deployment name." });
|
|
316
|
+
const REASONING_LEVELS = [
|
|
317
|
+
"minimal",
|
|
318
|
+
"low",
|
|
319
|
+
"medium",
|
|
320
|
+
"high"
|
|
321
|
+
];
|
|
322
|
+
const reasoningLevelSchema = z.enum(REASONING_LEVELS).optional().meta({ description: "Optional reasoning effort for reasoning models. Omit to let the model decide (the parameter is then not sent)." });
|
|
287
323
|
function parseLenientProvider(value) {
|
|
288
324
|
return value === "SCCH" || value === "Azure Foundry" ? value : void 0;
|
|
289
325
|
}
|
|
326
|
+
function parseLenientReasoningLevel(value) {
|
|
327
|
+
return typeof value === "string" && REASONING_LEVELS.includes(value) ? value : void 0;
|
|
328
|
+
}
|
|
290
329
|
//#endregion
|
|
291
330
|
//#region ../lib/registry-schema.ts
|
|
292
331
|
/** The fixed group names and the code module each one mints for. */
|
|
@@ -307,6 +346,7 @@ function timestampField(field) {
|
|
|
307
346
|
}, `${field} must not carry sub-second precision — the server stores whole seconds`);
|
|
308
347
|
}
|
|
309
348
|
const providerField = z.enum(LLM_PROVIDERS, { error: "must be \"SCCH\" or \"Azure Foundry\"" });
|
|
349
|
+
const reasoningField = z.enum(REASONING_LEVELS, { error: `must be one of ${REASONING_LEVELS.join(", ")}` });
|
|
310
350
|
/**
|
|
311
351
|
* One registry entry. Unknown extra properties are ACCEPTED and ignored so authors can
|
|
312
352
|
* annotate freely and a newer registry keeps working with an older CLI — which is why
|
|
@@ -320,8 +360,9 @@ const RegistryEntrySchema = z.looseObject({
|
|
|
320
360
|
note: z.string().trim().max(200, `note must be at most 200 characters`).optional().meta({ description: `Note shown in the codes list, at most 200 characters. No effect on behaviour.` }),
|
|
321
361
|
llm: z.looseObject({
|
|
322
362
|
provider: providerField.meta({ description: "LLM provider override for this code. Required when `llm` is present." }),
|
|
323
|
-
model: z.string().trim().min(1).max(256).meta({ description: "Model id (for Azure Foundry, the deployment name). Required when `llm` is present." })
|
|
324
|
-
|
|
363
|
+
model: z.string().trim().min(1).max(256).meta({ description: "Model id (for Azure Foundry, the deployment name). Required when `llm` is present." }),
|
|
364
|
+
reasoning: reasoningField.optional().meta({ description: "Optional reasoning effort for reasoning models, applied on top of the provider/model pair. Omit to let the model decide." })
|
|
365
|
+
}).optional().meta({ description: "Per-code LLM override replacing the activity YAML's own `llm:`. Provider and model must be given together; `reasoning` is optional on top of them." })
|
|
325
366
|
}).refine((entry) => entry.file === void 0 !== (entry.url === void 0), "give exactly one of `file` (relative to base-url) or `url` (absolute)").meta({
|
|
326
367
|
id: "registryEntry",
|
|
327
368
|
description: "One activity: where its YAML lives, plus the parameters its code is minted with."
|
|
@@ -451,7 +492,8 @@ function parseRegistry(text) {
|
|
|
451
492
|
note: entry.note ?? null,
|
|
452
493
|
llm: entry.llm ? {
|
|
453
494
|
provider: entry.llm.provider,
|
|
454
|
-
model: entry.llm.model
|
|
495
|
+
model: entry.llm.model,
|
|
496
|
+
...entry.llm.reasoning ? { reasoning: entry.llm.reasoning } : {}
|
|
455
497
|
} : null
|
|
456
498
|
});
|
|
457
499
|
}
|
|
@@ -521,7 +563,8 @@ function parseServerCodes(payload) {
|
|
|
521
563
|
validUntil: typeof value.validUntil === "string" ? value.validUntil : null,
|
|
522
564
|
llm: typeof llm === "object" && llm !== null ? {
|
|
523
565
|
provider: String(llm.provider ?? ""),
|
|
524
|
-
model: String(llm.model ?? "")
|
|
566
|
+
model: String(llm.model ?? ""),
|
|
567
|
+
reasoning: typeof llm.reasoning === "string" ? llm.reasoning : null
|
|
525
568
|
} : null,
|
|
526
569
|
createdAt: typeof value.createdAt === "string" ? value.createdAt : null
|
|
527
570
|
});
|
|
@@ -539,9 +582,16 @@ function sameInstant(a, b) {
|
|
|
539
582
|
const right = Date.parse(b);
|
|
540
583
|
return !Number.isNaN(left) && left === right;
|
|
541
584
|
}
|
|
585
|
+
/**
|
|
586
|
+
* The override compares WHOLE, reasoning level included: a code minted at a different
|
|
587
|
+
* effort serves different behavior, so it must not be reused. A differing level therefore
|
|
588
|
+
* fails the match and the entry mints a NEW code — sync never modifies an existing one
|
|
589
|
+
* (docs/registry.md). An absent level on either side compares as null, so an entry
|
|
590
|
+
* without `reasoning` keeps matching the codes minted before the field existed.
|
|
591
|
+
*/
|
|
542
592
|
function sameLlm(a, b) {
|
|
543
593
|
if (a === null || b === null) return a === b;
|
|
544
|
-
return a.provider === b.provider && a.model === b.model;
|
|
594
|
+
return a.provider === b.provider && a.model === b.model && (a.reasoning ?? null) === (b.reasoning ?? null);
|
|
545
595
|
}
|
|
546
596
|
/**
|
|
547
597
|
* The codes that ARE this entry: same activity URL, module and availability
|
|
@@ -846,7 +896,8 @@ async function readLock(lockPath) {
|
|
|
846
896
|
}
|
|
847
897
|
function registerCodes(program) {
|
|
848
898
|
const codes = program.command("codes").description("Manage activity codes on the Novedu server");
|
|
849
|
-
codes.command("create").description("Create a code for an activity YAML (validated server-side before storing)").requiredOption("--module <module>", "activity module: tutor, quiz, writing or coding").requiredOption("--file <url>", "public http(s) URL of the activity YAML").option("--start <iso>", "window start, ISO 8601 with explicit offset (e.g. 2026-07-07T08:00:00Z)").option("--end <iso>", "window end, ISO 8601 with explicit offset").option("--note <text>", "note shown in the codes list").option("--llm-provider <provider>", "LLM override provider (\"SCCH\" or \"Azure Foundry\"; needs --llm-model)").option("--llm-model <model>", "LLM override model id (needs --llm-provider)").option(...SERVER_OPTION$3).action(async (options) => {
|
|
899
|
+
codes.command("create").description("Create a code for an activity YAML (validated server-side before storing)").requiredOption("--module <module>", "activity module: tutor, quiz, writing or coding").requiredOption("--file <url>", "public http(s) URL of the activity YAML").option("--start <iso>", "window start, ISO 8601 with explicit offset (e.g. 2026-07-07T08:00:00Z)").option("--end <iso>", "window end, ISO 8601 with explicit offset").option("--note <text>", "note shown in the codes list").option("--llm-provider <provider>", "LLM override provider (\"SCCH\" or \"Azure Foundry\"; needs --llm-model)").option("--llm-model <model>", "LLM override model id (needs --llm-provider)").option("--llm-reasoning <level>", "LLM override reasoning effort (\"minimal\", \"low\", \"medium\" or \"high\"; needs the provider/model pair)").option(...SERVER_OPTION$3).action(async (options) => {
|
|
900
|
+
const llmGiven = options.llmProvider !== void 0 || options.llmModel !== void 0 || options.llmReasoning !== void 0;
|
|
850
901
|
await runApiRequest({
|
|
851
902
|
server: options.server,
|
|
852
903
|
path: "/api/codes",
|
|
@@ -857,10 +908,11 @@ function registerCodes(program) {
|
|
|
857
908
|
...options.start === void 0 ? {} : { validFrom: options.start },
|
|
858
909
|
...options.end === void 0 ? {} : { validUntil: options.end },
|
|
859
910
|
...options.note === void 0 ? {} : { note: options.note },
|
|
860
|
-
...
|
|
911
|
+
...llmGiven ? { llm: {
|
|
861
912
|
provider: options.llmProvider ?? "",
|
|
862
|
-
model: options.llmModel ?? ""
|
|
863
|
-
|
|
913
|
+
model: options.llmModel ?? "",
|
|
914
|
+
...options.llmReasoning === void 0 ? {} : { reasoning: options.llmReasoning }
|
|
915
|
+
} } : {}
|
|
864
916
|
}
|
|
865
917
|
});
|
|
866
918
|
});
|
|
@@ -1024,6 +1076,10 @@ function appendInstructions(existing, instructions) {
|
|
|
1024
1076
|
* client sent no system message, a leading one carrying only the teacher's instructions
|
|
1025
1077
|
* is added. Everything else (messages, tools, tool_choice, temperature, stream, …)
|
|
1026
1078
|
* passes through verbatim, so client-side tools and streaming are all preserved.
|
|
1079
|
+
*
|
|
1080
|
+
* A `reasoning` level (the effective activity/code setting) is pinned exactly like
|
|
1081
|
+
* `model` — it OVERWRITES whatever the client sent as `reasoning_effort`. Without one
|
|
1082
|
+
* the client's own `reasoning_effort` passes through untouched, like any other parameter.
|
|
1027
1083
|
*/
|
|
1028
1084
|
function buildUpstreamChatBody(clientBody, opts) {
|
|
1029
1085
|
const clientMessages = Array.isArray(clientBody.messages) ? clientBody.messages : [];
|
|
@@ -1046,6 +1102,7 @@ function buildUpstreamChatBody(clientBody, opts) {
|
|
|
1046
1102
|
model: opts.model,
|
|
1047
1103
|
messages
|
|
1048
1104
|
};
|
|
1105
|
+
if (opts.reasoning) upstream.reasoning_effort = opts.reasoning;
|
|
1049
1106
|
if (clientBody.stream === true) upstream.stream_options = {
|
|
1050
1107
|
...isRecord(clientBody.stream_options) ? clientBody.stream_options : {},
|
|
1051
1108
|
include_usage: true
|
|
@@ -1108,6 +1165,11 @@ function parseCoding(content) {
|
|
|
1108
1165
|
ok: false,
|
|
1109
1166
|
message: "This coding activity uses an unsupported llm.provider (use \"SCCH\" or \"Azure Foundry\")."
|
|
1110
1167
|
};
|
|
1168
|
+
const reasoning = llm?.reasoning === void 0 ? void 0 : parseLenientReasoningLevel(llm.reasoning);
|
|
1169
|
+
if (llm?.reasoning !== void 0 && !reasoning) return {
|
|
1170
|
+
ok: false,
|
|
1171
|
+
message: "This coding activity uses an unsupported llm.reasoning (use \"minimal\", \"low\", \"medium\" or \"high\")."
|
|
1172
|
+
};
|
|
1111
1173
|
const instructions = asString$2(root.instructions);
|
|
1112
1174
|
if (!instructions) return {
|
|
1113
1175
|
ok: false,
|
|
@@ -1120,6 +1182,7 @@ function parseCoding(content) {
|
|
|
1120
1182
|
title: asString$2(root.title),
|
|
1121
1183
|
model,
|
|
1122
1184
|
provider,
|
|
1185
|
+
reasoning,
|
|
1123
1186
|
instructions,
|
|
1124
1187
|
fragmentBlock: readFragmentBlock(root)
|
|
1125
1188
|
}
|
|
@@ -2411,6 +2474,11 @@ function parseQuiz(content) {
|
|
|
2411
2474
|
ok: false,
|
|
2412
2475
|
message: "This quiz uses an unsupported llm.provider (use \"SCCH\" or \"Azure Foundry\")."
|
|
2413
2476
|
};
|
|
2477
|
+
const reasoning = llm?.reasoning === void 0 ? void 0 : parseLenientReasoningLevel(llm.reasoning);
|
|
2478
|
+
if (llm?.reasoning !== void 0 && !reasoning) return {
|
|
2479
|
+
ok: false,
|
|
2480
|
+
message: "This quiz uses an unsupported llm.reasoning (use \"minimal\", \"low\", \"medium\" or \"high\")."
|
|
2481
|
+
};
|
|
2414
2482
|
const quizFiles = Array.isArray(root.quiz_files) ? root.quiz_files : [];
|
|
2415
2483
|
const rawQuestions = Array.isArray(root.questions) ? root.questions : [];
|
|
2416
2484
|
if (rawQuestions.length === 0 && quizFiles.length === 0) return {
|
|
@@ -2453,6 +2521,7 @@ function parseQuiz(content) {
|
|
|
2453
2521
|
shuffle: asBool$1(root.shuffle, true),
|
|
2454
2522
|
model,
|
|
2455
2523
|
provider,
|
|
2524
|
+
reasoning,
|
|
2456
2525
|
questionCount,
|
|
2457
2526
|
imageInput: asBool$1(llm?.imageInput, false),
|
|
2458
2527
|
discussionInstructions: asString$1(root.discussion?.instructions),
|
|
@@ -2686,6 +2755,7 @@ const TutorSchema = z.strictObject({
|
|
|
2686
2755
|
llm: z.strictObject({
|
|
2687
2756
|
model: z.string().meta({ description: "Model used for this tutor." }),
|
|
2688
2757
|
provider: providerSchema,
|
|
2758
|
+
reasoning: reasoningLevelSchema,
|
|
2689
2759
|
imageInput: z.boolean().optional().meta({
|
|
2690
2760
|
default: true,
|
|
2691
2761
|
description: "Image uploads are enabled by default. Set to false to hide the upload UI for text-only tutors or non-vision-capable models."
|
|
@@ -2734,6 +2804,7 @@ async function loadAndBuildTutorPrompt(url, fetchImpl, opts = {}) {
|
|
|
2734
2804
|
prompt: assembled.prompt,
|
|
2735
2805
|
model: tutor.llm.model,
|
|
2736
2806
|
provider: tutor.llm.provider,
|
|
2807
|
+
reasoning: tutor.llm.reasoning,
|
|
2737
2808
|
imageInput: tutor.llm.imageInput ?? true,
|
|
2738
2809
|
tools: tutor.tools,
|
|
2739
2810
|
anonymous: tutor.anonymous ?? true,
|
|
@@ -2784,6 +2855,11 @@ function parseWriting(content) {
|
|
|
2784
2855
|
ok: false,
|
|
2785
2856
|
message: "This writing activity uses an unsupported llm.provider (use \"SCCH\" or \"Azure Foundry\")."
|
|
2786
2857
|
};
|
|
2858
|
+
const reasoning = llm?.reasoning === void 0 ? void 0 : parseLenientReasoningLevel(llm.reasoning);
|
|
2859
|
+
if (llm?.reasoning !== void 0 && !reasoning) return {
|
|
2860
|
+
ok: false,
|
|
2861
|
+
message: "This writing activity uses an unsupported llm.reasoning (use \"minimal\", \"low\", \"medium\" or \"high\")."
|
|
2862
|
+
};
|
|
2787
2863
|
const instructions = asString(root.instructions);
|
|
2788
2864
|
if (!instructions) return {
|
|
2789
2865
|
ok: false,
|
|
@@ -2799,6 +2875,7 @@ function parseWriting(content) {
|
|
|
2799
2875
|
anonymous: asBool(root.anonymous, false),
|
|
2800
2876
|
model,
|
|
2801
2877
|
provider,
|
|
2878
|
+
reasoning,
|
|
2802
2879
|
instructions,
|
|
2803
2880
|
fragmentBlock: readFragmentBlock(root),
|
|
2804
2881
|
placeholder: asString(root.placeholder)
|
|
@@ -2905,7 +2982,8 @@ const promptDumpers = {
|
|
|
2905
2982
|
id: result.id,
|
|
2906
2983
|
llm: {
|
|
2907
2984
|
provider: result.provider,
|
|
2908
|
-
model: result.model
|
|
2985
|
+
model: result.model,
|
|
2986
|
+
...result.reasoning ? { reasoning: result.reasoning } : {}
|
|
2909
2987
|
},
|
|
2910
2988
|
system: result.prompt,
|
|
2911
2989
|
tools: result.tools
|
|
@@ -2923,7 +3001,8 @@ const promptDumpers = {
|
|
|
2923
3001
|
id: quiz.id,
|
|
2924
3002
|
llm: {
|
|
2925
3003
|
provider: quiz.provider,
|
|
2926
|
-
model: quiz.model
|
|
3004
|
+
model: quiz.model,
|
|
3005
|
+
...quiz.reasoning ? { reasoning: quiz.reasoning } : {}
|
|
2927
3006
|
},
|
|
2928
3007
|
grading: {
|
|
2929
3008
|
userMessageTemplate: QUIZ_ANSWER_MESSAGE_TEMPLATE,
|
|
@@ -2963,7 +3042,8 @@ const promptDumpers = {
|
|
|
2963
3042
|
id: writing.id,
|
|
2964
3043
|
llm: {
|
|
2965
3044
|
provider: writing.provider,
|
|
2966
|
-
model: writing.model
|
|
3045
|
+
model: writing.model,
|
|
3046
|
+
...writing.reasoning ? { reasoning: writing.reasoning } : {}
|
|
2967
3047
|
},
|
|
2968
3048
|
system: writing.instructions
|
|
2969
3049
|
}
|
|
@@ -2985,7 +3065,8 @@ const promptDumpers = {
|
|
|
2985
3065
|
id: coding.id,
|
|
2986
3066
|
llm: {
|
|
2987
3067
|
provider: coding.provider,
|
|
2988
|
-
model: coding.model
|
|
3068
|
+
model: coding.model,
|
|
3069
|
+
...coding.reasoning ? { reasoning: coding.reasoning } : {}
|
|
2989
3070
|
},
|
|
2990
3071
|
system: coding.instructions,
|
|
2991
3072
|
upstreamSystemMessage: typeof system?.content === "string" ? system.content : ""
|
|
@@ -3088,6 +3169,7 @@ const QuizYamlSchema = z.strictObject({
|
|
|
3088
3169
|
llm: z.strictObject({
|
|
3089
3170
|
model: z.string().min(1).meta({ description: "The model that grades answers and drives the per-question discussion chat." }),
|
|
3090
3171
|
provider: providerSchema,
|
|
3172
|
+
reasoning: reasoningLevelSchema,
|
|
3091
3173
|
imageInput: z.boolean().optional().meta({
|
|
3092
3174
|
default: false,
|
|
3093
3175
|
description: "Default for all questions: students may attach photos (up to 3, 5 MB each) to their answers. The model must be vision-capable. A per-question imageInput overrides it."
|
|
@@ -3641,6 +3723,31 @@ function createJudgeBreaker() {
|
|
|
3641
3723
|
}
|
|
3642
3724
|
/** Consecutive fully-errored judge calls that mean "stop judging for the rest of the run". */
|
|
3643
3725
|
const JUDGE_BREAKER_LIMIT = 3;
|
|
3726
|
+
/**
|
|
3727
|
+
* The spec a run's calls are actually served with, out of the TARGET activity's own spec
|
|
3728
|
+
* and the run's two override flags. TWO independent axes (docs/cli-eval.md):
|
|
3729
|
+
*
|
|
3730
|
+
* - the PAIR (`--llm-provider`/`--llm-model`) replaces provider+model **wholesale**, so a
|
|
3731
|
+
* pair given without a level DROPS the file's level — the same bundle semantics a
|
|
3732
|
+
* per-code LLM override has (`effectiveLlm`, docs/ai-models.md);
|
|
3733
|
+
* - the LEVEL (`--llm-reasoning`) replaces only the effort, on top of whichever pair won,
|
|
3734
|
+
* which is what makes "the file's own model, at high effort" a one-flag run.
|
|
3735
|
+
*
|
|
3736
|
+
* The judge's flags reuse this with the EFFECTIVE grading spec as the activity, which is
|
|
3737
|
+
* why "no judge flag" means "judge exactly like the model under test", level included.
|
|
3738
|
+
*/
|
|
3739
|
+
function resolveEvalSpec(activity, pair, reasoning) {
|
|
3740
|
+
const base = pair ?? activity;
|
|
3741
|
+
return reasoning ? {
|
|
3742
|
+
provider: base.provider,
|
|
3743
|
+
model: base.model,
|
|
3744
|
+
reasoning
|
|
3745
|
+
} : base;
|
|
3746
|
+
}
|
|
3747
|
+
/** Do two specs describe the same call? Provider, model AND effort — all three matter. */
|
|
3748
|
+
function sameEvalSpec(a, b) {
|
|
3749
|
+
return a.provider === b.provider && a.model === b.model && a.reasoning === b.reasoning;
|
|
3750
|
+
}
|
|
3644
3751
|
/** Narrow a case to the tutor arm. */
|
|
3645
3752
|
function isTutorCase(evalCase) {
|
|
3646
3753
|
return "conversation" in evalCase;
|
|
@@ -4333,7 +4440,7 @@ function formatEvalResult(result, source) {
|
|
|
4333
4440
|
lines.push(` target: ${result.targetUrl}`);
|
|
4334
4441
|
if (result.kind === "tutor") lines.push(` conversations: ${result.caseCount}`);
|
|
4335
4442
|
else lines.push(` questions: ${result.evalFile.questions.length} cases: ${result.caseCount}`);
|
|
4336
|
-
lines.push(` ${result.kind} model: ${result.llm
|
|
4443
|
+
lines.push(` ${result.kind} model: ${llmSpecText(result.llm)}`);
|
|
4337
4444
|
if (result.warnings.length) {
|
|
4338
4445
|
lines.push("");
|
|
4339
4446
|
lines.push(yellow(`${result.warnings.length} warning(s):`));
|
|
@@ -4387,6 +4494,14 @@ function mismatchLines(result) {
|
|
|
4387
4494
|
});
|
|
4388
4495
|
}
|
|
4389
4496
|
/**
|
|
4497
|
+
* One llm spec as the reports name it: `SCCH / gemma-4`, with ` (reasoning: high)`
|
|
4498
|
+
* appended whenever an effort level applies — two runs of one model at different efforts
|
|
4499
|
+
* behave differently, so the level belongs in the header.
|
|
4500
|
+
*/
|
|
4501
|
+
function llmSpecText(spec) {
|
|
4502
|
+
return `${spec.provider} / ${spec.model}${spec.reasoning ? ` (reasoning: ${spec.reasoning})` : ""}`;
|
|
4503
|
+
}
|
|
4504
|
+
/**
|
|
4390
4505
|
* The human report for ONE eval run: header (id, target, the EFFECTIVE llm — rendered
|
|
4391
4506
|
* as `quiz-llm → override-llm` when `--llm-provider`/`--llm-model` was used, so a
|
|
4392
4507
|
* comparison report can never be mistaken for a baseline one), one line per
|
|
@@ -4399,10 +4514,10 @@ function formatEvalReport(result, source) {
|
|
|
4399
4514
|
` id: ${result.id}`,
|
|
4400
4515
|
` target: ${result.target}`
|
|
4401
4516
|
];
|
|
4402
|
-
const llm = result.llm.overrides ? `${result.llm.overrides
|
|
4517
|
+
const llm = result.llm.overrides ? `${llmSpecText(result.llm.overrides)} ${yellow("→")} ${llmSpecText(result.llm)} ${yellow("(override)")}` : llmSpecText(result.llm);
|
|
4403
4518
|
lines.push(` llm: ${llm}`);
|
|
4404
4519
|
const judge = result.llm.judge;
|
|
4405
|
-
if (judge && (judge.provider !== result.llm.provider || judge.model !== result.llm.model)) lines.push(` judge llm: ${judge
|
|
4520
|
+
if (judge && (judge.provider !== result.llm.provider || judge.model !== result.llm.model || judge.reasoning !== result.llm.reasoning)) lines.push(` judge llm: ${llmSpecText(judge)}${judge.overridden ? ` ${yellow("(override)")}` : ""}`);
|
|
4406
4521
|
const unit = result.kind === "tutor" ? "conversation" : "case";
|
|
4407
4522
|
const generation = result.kind === "tutor" ? "generation" : "grading";
|
|
4408
4523
|
lines.push(` ${unit}s: ${totals.cases} × ${totals.repeats} repeat(s) = ${totals.calls} ${generation} call(s)` + (result.judging === "off" ? "" : ` + ${totals.calls} judge call(s)`));
|
|
@@ -4494,7 +4609,7 @@ function shortSource$1(source) {
|
|
|
4494
4609
|
function formatPromptDump(dump, sections, source) {
|
|
4495
4610
|
const lines = [green(`✔ Prompts — ${dump.kind}`) + dim(` — ${source}`)];
|
|
4496
4611
|
lines.push(` id: ${dump.id}`);
|
|
4497
|
-
lines.push(` provider: ${dump.llm.provider} model: ${dump.llm.model}`);
|
|
4612
|
+
lines.push(` provider: ${dump.llm.provider} model: ${dump.llm.model}` + (dump.llm.reasoning ? ` reasoning: ${dump.llm.reasoning}` : ""));
|
|
4498
4613
|
if (dump.kind === "tutor" && dump.tools.length > 0) lines.push(` tools: ${dump.tools.join(", ")}`);
|
|
4499
4614
|
lines.push(` prompts: ${sections.length}`);
|
|
4500
4615
|
for (const section of sections) lines.push(` ${section.name}: ${section.text.length} chars`);
|
|
@@ -4530,10 +4645,18 @@ function inline(text) {
|
|
|
4530
4645
|
function quote(text) {
|
|
4531
4646
|
return text.replace(/\s+$/, "").split(/\r?\n/).map((line) => line ? `> ${line}` : ">").join("\n");
|
|
4532
4647
|
}
|
|
4648
|
+
/**
|
|
4649
|
+
* One spec: `SCCH / gemma-4`, and `Azure Foundry / gpt-5.6-terra (reasoning: high)` when
|
|
4650
|
+
* an effort level applies. The level is part of a run's identity — two runs of one model
|
|
4651
|
+
* at different efforts produce different behavior — so it must be readable off the report.
|
|
4652
|
+
*/
|
|
4653
|
+
function specText(spec) {
|
|
4654
|
+
return `${spec.provider} / ${spec.model}${spec.reasoning ? ` (reasoning: ${spec.reasoning})` : ""}`;
|
|
4655
|
+
}
|
|
4533
4656
|
/** `SCCH / gemma-4`, or `SCCH / gemma-4 → Azure Foundry / gpt-5-mini (override)`. */
|
|
4534
4657
|
function llmText(llm) {
|
|
4535
|
-
const effective =
|
|
4536
|
-
return llm.overrides ? `${llm.overrides
|
|
4658
|
+
const effective = specText(llm);
|
|
4659
|
+
return llm.overrides ? `${specText(llm.overrides)} → ${effective} (override)` : effective;
|
|
4537
4660
|
}
|
|
4538
4661
|
/**
|
|
4539
4662
|
* The judge's pair, but ONLY when it differs from the grading pair — a judge line that
|
|
@@ -4543,8 +4666,8 @@ function llmText(llm) {
|
|
|
4543
4666
|
function judgeLlmText(llm) {
|
|
4544
4667
|
const judge = llm.judge;
|
|
4545
4668
|
if (!judge) return void 0;
|
|
4546
|
-
if (judge.provider === llm.provider && judge.model === llm.model) return
|
|
4547
|
-
return `${judge
|
|
4669
|
+
if (judge.provider === llm.provider && judge.model === llm.model && judge.reasoning === llm.reasoning) return;
|
|
4670
|
+
return `${specText(judge)}${judge.overridden ? " (override)" : ""}`;
|
|
4548
4671
|
}
|
|
4549
4672
|
/** `15,420 / 12,300 / 2,810`, or an em dash when nothing was reported. */
|
|
4550
4673
|
function usageCell(usage) {
|
|
@@ -4945,7 +5068,8 @@ const CodingYamlSchema = z.strictObject({
|
|
|
4945
5068
|
title: z.string().optional().meta({ description: "Optional label shown to the student on the /<code> connection page." }),
|
|
4946
5069
|
llm: z.strictObject({
|
|
4947
5070
|
model: z.string().min(1).meta({ description: "The model that answers. SERVER-ONLY and PINNED: the proxy always uses this model and ignores whatever model the coding agent sends." }),
|
|
4948
|
-
provider: providerSchema
|
|
5071
|
+
provider: providerSchema,
|
|
5072
|
+
reasoning: reasoningLevelSchema
|
|
4949
5073
|
}).meta({
|
|
4950
5074
|
id: "llm",
|
|
4951
5075
|
description: "The pinned model and provider that answer coding requests."
|
|
@@ -5022,7 +5146,8 @@ const WritingYamlSchema = z.strictObject({
|
|
|
5022
5146
|
}),
|
|
5023
5147
|
llm: z.strictObject({
|
|
5024
5148
|
model: z.string().min(1).meta({ description: "The model that drives the feedback chat." }),
|
|
5025
|
-
provider: providerSchema
|
|
5149
|
+
provider: providerSchema,
|
|
5150
|
+
reasoning: reasoningLevelSchema
|
|
5026
5151
|
}).meta({
|
|
5027
5152
|
id: "llm",
|
|
5028
5153
|
description: "The model and provider that back the writing coach."
|
|
@@ -5277,6 +5402,26 @@ function parsePair(flag, provider, model) {
|
|
|
5277
5402
|
};
|
|
5278
5403
|
}
|
|
5279
5404
|
/**
|
|
5405
|
+
* One reasoning-effort flag: absent, or one of the four known levels. Checked here rather
|
|
5406
|
+
* than left to the server for the same reason `parsePair` checks the provider — a typo
|
|
5407
|
+
* must cost nothing, not a whole run's worth of terminal 400s.
|
|
5408
|
+
*
|
|
5409
|
+
* Deliberately INDEPENDENT of its pair flag (unlike the pair's both-or-nothing rule): the
|
|
5410
|
+
* common comparison run is "same model, different effort", so `--llm-reasoning` alone is
|
|
5411
|
+
* a first-class invocation rather than a usage error.
|
|
5412
|
+
*/
|
|
5413
|
+
function parseReasoning(flag, value) {
|
|
5414
|
+
if (value === void 0) return { ok: true };
|
|
5415
|
+
if (!REASONING_LEVELS.includes(value)) return {
|
|
5416
|
+
ok: false,
|
|
5417
|
+
message: `Unknown --${flag} "${value}": expected ${REASONING_LEVELS.map((level) => `"${level}"`).join(", ")}.`
|
|
5418
|
+
};
|
|
5419
|
+
return {
|
|
5420
|
+
ok: true,
|
|
5421
|
+
reasoning: value
|
|
5422
|
+
};
|
|
5423
|
+
}
|
|
5424
|
+
/**
|
|
5280
5425
|
* The optional `usage: { input, cachedInput, output }` of a 200 response, defensively:
|
|
5281
5426
|
* anything that is not three finite numbers is simply absent (an older server, or one
|
|
5282
5427
|
* whose provider reports nothing, must never break a run).
|
|
@@ -5526,14 +5671,24 @@ async function runEvalCommand(pathsOrUrls, options, seams = {}) {
|
|
|
5526
5671
|
failJson({ message: override.message });
|
|
5527
5672
|
return;
|
|
5528
5673
|
}
|
|
5674
|
+
const overrideReasoning = parseReasoning("llm-reasoning", options.llmReasoning);
|
|
5675
|
+
if (!overrideReasoning.ok) {
|
|
5676
|
+
failJson({ message: overrideReasoning.message });
|
|
5677
|
+
return;
|
|
5678
|
+
}
|
|
5529
5679
|
const judgeOverride = parsePair("judge-llm", options.judgeLlmProvider, options.judgeLlmModel);
|
|
5530
5680
|
if (!judgeOverride.ok) {
|
|
5531
5681
|
failJson({ message: judgeOverride.message });
|
|
5532
5682
|
return;
|
|
5533
5683
|
}
|
|
5684
|
+
const judgeOverrideReasoning = parseReasoning("judge-llm-reasoning", options.judgeLlmReasoning);
|
|
5685
|
+
if (!judgeOverrideReasoning.ok) {
|
|
5686
|
+
failJson({ message: judgeOverrideReasoning.message });
|
|
5687
|
+
return;
|
|
5688
|
+
}
|
|
5534
5689
|
const judging = options.judgeFeedback !== false;
|
|
5535
|
-
if (!judging && judgeOverride.llm) {
|
|
5536
|
-
failJson({ message: "--judge-llm-provider/--judge-llm-model cannot be combined with --no-judge-feedback: the first
|
|
5690
|
+
if (!judging && (judgeOverride.llm || judgeOverrideReasoning.reasoning)) {
|
|
5691
|
+
failJson({ message: "--judge-llm-provider/--judge-llm-model/--judge-llm-reasoning cannot be combined with --no-judge-feedback: the first configure the feedback judge, the second switches it off." });
|
|
5537
5692
|
return;
|
|
5538
5693
|
}
|
|
5539
5694
|
const expansion = expandSources(pathsOrUrls);
|
|
@@ -5599,16 +5754,17 @@ async function runEvalCommand(pathsOrUrls, options, seams = {}) {
|
|
|
5599
5754
|
if (!check) continue;
|
|
5600
5755
|
const activityLlm = {
|
|
5601
5756
|
provider: check.llm.provider,
|
|
5602
|
-
model: check.llm.model
|
|
5757
|
+
model: check.llm.model,
|
|
5758
|
+
...check.llm.reasoning ? { reasoning: check.llm.reasoning } : {}
|
|
5603
5759
|
};
|
|
5604
|
-
const effective = override.llm
|
|
5605
|
-
const judgeLlm = judgeOverride.llm
|
|
5760
|
+
const effective = resolveEvalSpec(activityLlm, override.llm, overrideReasoning.reasoning);
|
|
5761
|
+
const judgeLlm = resolveEvalSpec(effective, judgeOverride.llm, judgeOverrideReasoning.reasoning);
|
|
5606
5762
|
const llm = {
|
|
5607
5763
|
...effective,
|
|
5608
|
-
...
|
|
5764
|
+
...sameEvalSpec(effective, activityLlm) ? {} : { overrides: activityLlm },
|
|
5609
5765
|
...judging ? { judge: {
|
|
5610
5766
|
...judgeLlm,
|
|
5611
|
-
overridden: judgeOverride.llm !== void 0
|
|
5767
|
+
overridden: judgeOverride.llm !== void 0 || judgeOverrideReasoning.reasoning !== void 0
|
|
5612
5768
|
} } : {}
|
|
5613
5769
|
};
|
|
5614
5770
|
const label = files.length > 1 ? `(${fileIndex}/${files.length}) ${check.evalFile.id}` : check.evalFile.id;
|
|
@@ -5654,7 +5810,7 @@ async function runEvalCommand(pathsOrUrls, options, seams = {}) {
|
|
|
5654
5810
|
process.exitCode = batchPassed(batch) ? 0 : 1;
|
|
5655
5811
|
}
|
|
5656
5812
|
function registerEval(program) {
|
|
5657
|
-
program.command("eval").description("Run an eval file (quiz golden answers, or tutor conversations) against the real activity path and report the result").argument("<evalPathOrUrl...>", "one or more eval YAML files (paths, http(s)/file URLs, or a quoted glob pattern)").option("--server <url>", "Novedu server base URL (defaults to the NOVEDU_SERVER env var, then production)").option("--concurrency <n>", "cases in flight per file", String(CONCURRENCY_DEFAULT)).option("--repeats <n>", "run every case N times (quiz: take the majority verdict)", "1").option("--llm-provider <provider>", "run with this provider instead of the activity's (\"SCCH\" or \"Azure Foundry\"; needs --llm-model)").option("--llm-model <model>", "run with this model instead of the activity's (needs --llm-provider)").option("--no-judge-feedback", "skip the LLM audit of what the model wrote (halves the LLM calls)").option("--judge-llm-provider <provider>", "judge with this provider (\"SCCH\" or \"Azure Foundry\"; needs --judge-llm-model)").option("--judge-llm-model <model>", "judge with this model instead of the one under test (needs --judge-llm-provider)").option("--json", "print the machine-readable batch report on stdout").option("--out <file>", "additionally write the machine-readable batch report to a file").option("--report <file>", "additionally write a readable Markdown report to a file").addHelpText("after", `
|
|
5813
|
+
program.command("eval").description("Run an eval file (quiz golden answers, or tutor conversations) against the real activity path and report the result").argument("<evalPathOrUrl...>", "one or more eval YAML files (paths, http(s)/file URLs, or a quoted glob pattern)").option("--server <url>", "Novedu server base URL (defaults to the NOVEDU_SERVER env var, then production)").option("--concurrency <n>", "cases in flight per file", String(CONCURRENCY_DEFAULT)).option("--repeats <n>", "run every case N times (quiz: take the majority verdict)", "1").option("--llm-provider <provider>", "run with this provider instead of the activity's (\"SCCH\" or \"Azure Foundry\"; needs --llm-model)").option("--llm-model <model>", "run with this model instead of the activity's (needs --llm-provider)").option("--llm-reasoning <level>", "run at this reasoning effort (\"minimal\", \"low\", \"medium\" or \"high\"); on its own it keeps the activity's model").option("--no-judge-feedback", "skip the LLM audit of what the model wrote (halves the LLM calls)").option("--judge-llm-provider <provider>", "judge with this provider (\"SCCH\" or \"Azure Foundry\"; needs --judge-llm-model)").option("--judge-llm-model <model>", "judge with this model instead of the one under test (needs --judge-llm-provider)").option("--judge-llm-reasoning <level>", "judge at this reasoning effort (\"minimal\", \"low\", \"medium\" or \"high\"); on its own it keeps the judge's model").option("--json", "print the machine-readable batch report on stdout").option("--out <file>", "additionally write the machine-readable batch report to a file").option("--report <file>", "additionally write a readable Markdown report to a file").addHelpText("after", `
|
|
5658
5814
|
Examples:
|
|
5659
5815
|
# Evaluate one quiz's golden answers
|
|
5660
5816
|
$ novedu-cli eval ./0010-welcome-quiz.eval.yaml
|
|
@@ -5672,6 +5828,9 @@ Examples:
|
|
|
5672
5828
|
# How would this rubric perform on another model? (both flags, always together)
|
|
5673
5829
|
$ novedu-cli eval ./my-quiz.eval.yaml --llm-provider "Azure Foundry" --llm-model gpt-5-mini
|
|
5674
5830
|
|
|
5831
|
+
# Same model, more thinking: the level alone keeps the activity's provider/model
|
|
5832
|
+
$ novedu-cli eval ./my-quiz.eval.yaml --llm-reasoning high
|
|
5833
|
+
|
|
5675
5834
|
# A strong judge over the quiz's own grader — the recommended pairing
|
|
5676
5835
|
$ novedu-cli eval ./my-quiz.eval.yaml --judge-llm-provider "Azure Foundry" --judge-llm-model gpt-5.6-terra
|
|
5677
5836
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@novedu/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.1",
|
|
4
4
|
"description": "Command-line companion for the Novedu chat app. Validates tutor, fragment, quiz, writing, coding and eval YAML definitions, dumps the exact LLM prompts an activity produces, evaluates a quiz's grading rubric against golden answers and replays scripted conversations against a tutor; signs in with Entra ID and manages codes, app-hosted files and images over the app's API.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|