@thanh01.pmt/curriculum-kit 1.4.32 → 1.4.34

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/dist/index.cjs CHANGED
@@ -991,6 +991,52 @@ var init_streamRunner = __esm({
991
991
  exports.DEFAULT_ARTIFACT_STREAM_TOTAL_MS = 48e4;
992
992
  }
993
993
  });
994
+ function cleanNonStreamingFetch() {
995
+ return async (input, init) => {
996
+ const res = await fetch(input, init);
997
+ let wantsStream = false;
998
+ try {
999
+ const body = init?.body;
1000
+ if (typeof body === "string") wantsStream = /"stream"\s*:\s*true/.test(body);
1001
+ } catch {
1002
+ }
1003
+ if (wantsStream) return res;
1004
+ const contentType = res.headers.get("content-type") || "";
1005
+ if (!contentType.includes("text/event-stream") && !contentType.includes("application/json")) return res;
1006
+ const text = await res.text();
1007
+ const trimmed = text.trim();
1008
+ if (trimmed.startsWith("data:")) {
1009
+ let content = "";
1010
+ let lastChunk = null;
1011
+ for (const line of trimmed.split("\n")) {
1012
+ const l = line.trim();
1013
+ if (!l || !l.startsWith("data:")) continue;
1014
+ const jsonStr = l.slice(5).trim();
1015
+ if (jsonStr === "[DONE]") continue;
1016
+ try {
1017
+ const chunk = JSON.parse(jsonStr);
1018
+ lastChunk = chunk;
1019
+ content += chunk.choices?.[0]?.delta?.content || "";
1020
+ } catch {
1021
+ }
1022
+ }
1023
+ const assembled = {
1024
+ id: lastChunk?.id || "chatcmpl-" + Date.now(),
1025
+ object: "chat.completion",
1026
+ created: lastChunk?.created || Math.floor(Date.now() / 1e3),
1027
+ model: lastChunk?.model || "9router",
1028
+ choices: [{ index: 0, message: { role: "assistant", content }, finish_reason: "stop" }],
1029
+ usage: lastChunk?.usage || void 0
1030
+ };
1031
+ const headers = new Headers(res.headers);
1032
+ headers.set("content-type", "application/json");
1033
+ return new Response(JSON.stringify(assembled), { status: res.status, statusText: res.statusText, headers });
1034
+ }
1035
+ const doneIndex = text.indexOf("data: [DONE]");
1036
+ if (doneIndex === -1) return res;
1037
+ return new Response(text.slice(0, doneIndex).trim(), { status: res.status, statusText: res.statusText, headers: res.headers });
1038
+ };
1039
+ }
994
1040
  function getAIModel(options = {}) {
995
1041
  const designated = getDesignatedFallbackConfig();
996
1042
  const defaultProvider = process.env.DEFAULT_AI_PROVIDER || process.env.LLM_PROVIDER || detectDefaultProvider();
@@ -1009,7 +1055,8 @@ function getAIModel(options = {}) {
1009
1055
  const rawUrl = options.baseURL || resolveApiKey("NINEROUTER_BASE_URL") || resolveApiKey("NINE_ROUTER_BASE_URL") || "https://ai-router.orchable.app/v1";
1010
1056
  const ninerouter = openai.createOpenAI({
1011
1057
  apiKey,
1012
- baseURL: rawUrl.replace(/^["']|["']$/g, "").trim()
1058
+ baseURL: rawUrl.replace(/^["']|["']$/g, "").trim(),
1059
+ fetch: cleanNonStreamingFetch()
1013
1060
  });
1014
1061
  return ninerouter.chat(resolvedModelName || "fast-reasoning");
1015
1062
  }
@@ -29472,7 +29519,7 @@ function safeParseJson(rawText) {
29472
29519
  }
29473
29520
  return null;
29474
29521
  }
29475
- async function streamLLMWithFallback(systemPrompt, userPrompt, apiKeys = {}, onChunk, preferredModel, preferredProvider, budget, signal) {
29522
+ async function streamLLMWithFallback(systemPrompt, userPrompt, apiKeys = {}, onChunk, preferredModel, preferredProvider, budget, signal, onEvent) {
29476
29523
  const alibabaKey = apiKeys.alibaba || apiKeys.dashscope || process.env.ALIBABA_API_KEY || process.env.DASHSCOPE_API_KEY;
29477
29524
  const nvidiaKey = apiKeys.nvidia || process.env.NVIDIA_API_KEY;
29478
29525
  const deepseekKey = apiKeys.deepseek || process.env.DEEPSEEK_API_KEY;
@@ -29553,12 +29600,14 @@ THINKING DIRECTIVE: Do ALL your reasoning internally BEFORE emitting output. Do
29553
29600
  idleMs: budget?.idleMs ?? DEFAULT_STREAM_IDLE_MS,
29554
29601
  totalMs: budget?.totalMs ?? DEFAULT_STREAM_TOTAL_MS
29555
29602
  };
29556
- for (const candidate of candidates) {
29603
+ for (const [candidateIdx, candidate] of candidates.entries()) {
29557
29604
  if (signal?.aborted) {
29558
29605
  console.log(`[curriculum-kit:VercelAI] AbortSignal already aborted, cancelling candidate loop.`);
29559
29606
  break;
29560
29607
  }
29561
29608
  const t0 = Date.now();
29609
+ const attempt = candidateIdx + 1;
29610
+ onEvent?.({ type: "candidate-start", provider: candidate.provider, model: candidate.model, attempt, total: candidates.length, elapsedMs: 0 });
29562
29611
  const abort = createStreamAbortSignal(resolvedBudget, signal);
29563
29612
  try {
29564
29613
  const modelInstance = getAIModel({
@@ -29566,18 +29615,35 @@ THINKING DIRECTIVE: Do ALL your reasoning internally BEFORE emitting output. Do
29566
29615
  modelName: candidate.model,
29567
29616
  apiKey: candidate.apiKey
29568
29617
  });
29618
+ const wireFetch = (input, init) => fetch(input, init).then((res) => {
29619
+ if (!res.body) return res;
29620
+ const kicked = res.body.pipeThrough(
29621
+ new TransformStream({
29622
+ transform(chunk, ctrl) {
29623
+ abort.kick();
29624
+ ctrl.enqueue(chunk);
29625
+ }
29626
+ })
29627
+ );
29628
+ return new Response(kicked, res);
29629
+ });
29569
29630
  const streamResult = ai.streamText({
29570
29631
  model: modelInstance,
29571
29632
  system: systemInstructions,
29572
29633
  prompt: userPrompt,
29573
29634
  temperature: 0.2,
29574
29635
  includeRawChunks: true,
29575
- abortSignal: abort.signal
29636
+ abortSignal: abort.signal,
29637
+ fetch: wireFetch
29576
29638
  });
29577
29639
  let fullContent = "";
29578
29640
  const extract = createStreamChunkExtractor();
29579
29641
  for await (const part of streamResult.fullStream) {
29580
29642
  abort.kick();
29643
+ if (part?.type === "error") {
29644
+ const errText = part.errorText ?? String(part.error ?? "unknown stream error");
29645
+ throw new Error(`[${candidate.provider}] stream error part: ${errText}`);
29646
+ }
29581
29647
  const extracted = extract(part);
29582
29648
  if (extracted.thought) onChunk?.(extracted.thought, "thought");
29583
29649
  if (extracted.content) {
@@ -29587,10 +29653,14 @@ THINKING DIRECTIVE: Do ALL your reasoning internally BEFORE emitting output. Do
29587
29653
  }
29588
29654
  if (fullContent.trim()) {
29589
29655
  console.log(`[curriculum-kit:VercelAI] \u2705 ${candidate.provider} (${candidate.model}) streamed in ${Date.now() - t0}ms`);
29656
+ onEvent?.({ type: "candidate-success", provider: candidate.provider, model: candidate.model, attempt, total: candidates.length, elapsedMs: Date.now() - t0 });
29590
29657
  return fullContent.trim();
29591
29658
  }
29659
+ console.warn(`[curriculum-kit:VercelAI] Candidate ${candidate.provider} (${candidate.model}) returned EMPTY content in ${Date.now() - t0}ms \u2192 trying next candidate.`);
29660
+ onEvent?.({ type: "candidate-failed", provider: candidate.provider, model: candidate.model, attempt, total: candidates.length, elapsedMs: Date.now() - t0, error: "empty content" });
29592
29661
  } catch (e) {
29593
29662
  console.warn(`[curriculum-kit:VercelAI] Candidate ${candidate.provider} (${candidate.model}) failed in ${Date.now() - t0}ms:`, e?.message || e);
29663
+ onEvent?.({ type: "candidate-failed", provider: candidate.provider, model: candidate.model, attempt, total: candidates.length, elapsedMs: Date.now() - t0, error: e?.message || String(e) });
29594
29664
  const isClientAborted = Boolean(
29595
29665
  signal?.aborted || e?.name === "AbortError" && signal?.aborted || e?.message && (e.message.includes("Controller is already closed") || e.message.includes("The operation was aborted") || e.message.includes("Aborted by parent signal"))
29596
29666
  );
@@ -29650,7 +29720,8 @@ Return concise JSON matching:
29650
29720
  model: options.model,
29651
29721
  provider: options.provider
29652
29722
  }),
29653
- options.signal
29723
+ options.signal,
29724
+ options.onProviderEvent
29654
29725
  );
29655
29726
  let parsedResearch = {
29656
29727
  groundTruthSummary: `Verified ecosystem standards for ${projectName}`,
@@ -29732,6 +29803,23 @@ async function streamLayerPrefillWithFallback(options, onChunk) {
29732
29803
  const research = await conductPreliminaryResearch(options, onChunk);
29733
29804
  return { success: true, research };
29734
29805
  }
29806
+ const runWithCustomInference = async (systemInstruction, userPrompt2) => {
29807
+ if (!options.customInference) return "";
29808
+ return options.customInference({
29809
+ systemInstruction,
29810
+ userPrompt: userPrompt2,
29811
+ messages: [{ role: "user", content: userPrompt2 }],
29812
+ temperature: 0.2,
29813
+ // Layer 3 synthesizes full context — give reasoning models the same
29814
+ // generous ceiling the artifact paths use (32k) so JSON is never truncated.
29815
+ maxTokens: 32768,
29816
+ onChunk: (token, type) => {
29817
+ if (type === "content") rawContent += token;
29818
+ if (type === "content" || type === "thought") onChunk?.(token, type);
29819
+ }
29820
+ });
29821
+ };
29822
+ let rawContent = "";
29735
29823
  const {
29736
29824
  projectName = "Curriculum Course",
29737
29825
  projectCode = "curriculum-course",
@@ -29843,27 +29931,31 @@ FULL ACCUMULATED CONTEXT (ALL CONFIRMED PARAMETERS FROM EVERY PREVIOUS STEP \u20
29843
29931
  ${buildRemainingContextBlock(accumulatedData) || "(no additional context)"}
29844
29932
 
29845
29933
  REQUIREMENT: Synthesize ALL parameters above \u2014 every single line in FULL ACCUMULATED CONTEXT is a confirmed user decision or system constraint and MUST influence the generated configuration. Produce the high-fidelity pre-fill configuration for LAYER ${targetLayer}.`;
29846
- let rawContent = "";
29847
- await streamLLMWithFallback(
29848
- systemPrompt,
29849
- userPrompt,
29850
- apiKeys,
29851
- (chunk, type) => {
29852
- if (type === "content") rawContent += chunk;
29853
- onChunk?.(chunk, type);
29854
- },
29855
- options.model,
29856
- options.provider,
29857
- // RC-W1: layer-aware budget — Layer 3 synthesizes full context and free-tier
29858
- // models (especially NVIDIA NIM) may think/stream for minutes. Idle window still catches hangs.
29859
- resolveStreamBudget(targetLayer, {
29860
- idleMs: options.idleTimeoutMs,
29861
- totalMs: options.timeoutMs,
29862
- model: options.model,
29863
- provider: options.provider
29864
- }),
29865
- options.signal
29866
- );
29934
+ if (options.customInference) {
29935
+ rawContent = await runWithCustomInference(systemPrompt, userPrompt) || "";
29936
+ } else {
29937
+ await streamLLMWithFallback(
29938
+ systemPrompt,
29939
+ userPrompt,
29940
+ apiKeys,
29941
+ (chunk, type) => {
29942
+ if (type === "content") rawContent += chunk;
29943
+ onChunk?.(chunk, type);
29944
+ },
29945
+ options.model,
29946
+ options.provider,
29947
+ // RC-W1: layer-aware budget — Layer 3 synthesizes full context and free-tier
29948
+ // models (especially NVIDIA NIM) may think/stream for minutes. Idle window still catches hangs.
29949
+ resolveStreamBudget(targetLayer, {
29950
+ idleMs: options.idleTimeoutMs,
29951
+ totalMs: options.timeoutMs,
29952
+ model: options.model,
29953
+ provider: options.provider
29954
+ }),
29955
+ options.signal,
29956
+ options.onProviderEvent
29957
+ );
29958
+ }
29867
29959
  const parsed = safeParseJson(rawContent);
29868
29960
  return {
29869
29961
  success: !!parsed,