u-foo 3.0.1 → 3.0.2
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/package.json +1 -1
- package/src/agents/prompts/native/tasks.js +4 -1
- package/src/app/chat/commandExecutor.js +111 -1
- package/src/app/chat/commands.js +2 -1
- package/src/app/chat/daemonMessageRouter.js +1 -1
- package/src/app/chat/inputSubmitHandler.js +3 -2
- package/src/code/commands.js +3 -3
- package/src/code/context/promptLayers.js +10 -9
- package/src/code/dispatch.js +4 -0
- package/src/code/modelCommand.js +199 -23
- package/src/code/nativeRunner.js +157 -19
- package/src/code/protocol/protocolValidator.js +3 -3
- package/src/code/providers/index.js +1 -0
- package/src/code/providers/modelsCatalog.js +304 -0
- package/src/code/repl.js +37 -8
- package/src/code/runtime/taskControl.js +177 -53
- package/src/code/runtime/taskFocus.js +30 -10
- package/src/code/runtime/taskLoop.js +12 -1
- package/src/code/runtime/taskRun.js +10 -1
- package/src/code/thinkingLevels.js +132 -0
- package/src/code/tools/taskRun.js +118 -0
- package/src/config.js +10 -1
- package/src/ui/format/index.js +48 -3
- package/src/ui/ink/ChatApp.js +137 -25
- package/src/ui/ink/UcodeApp.js +28 -3
- package/src/ui/ink/chatLogModel.js +238 -32
- package/src/ui/ink/chatReducer.js +18 -6
package/src/code/nativeRunner.js
CHANGED
|
@@ -5,6 +5,7 @@ const {
|
|
|
5
5
|
resolveKimiUpstreamCredentials,
|
|
6
6
|
} = require("../agents/providers/credentials/kimi");
|
|
7
7
|
const { runToolCall } = require("./dispatch");
|
|
8
|
+
const { runTaskRunTool } = require("./tools/taskRun");
|
|
8
9
|
const { appendUsageRecord } = require("./usageStore");
|
|
9
10
|
const {
|
|
10
11
|
persistToolResultToContext,
|
|
@@ -60,9 +61,11 @@ const CORE_TOOL_NAMES = new Set([
|
|
|
60
61
|
"bash",
|
|
61
62
|
"artifact_read",
|
|
62
63
|
"plan_graph",
|
|
64
|
+
"task_run",
|
|
63
65
|
"ask_user",
|
|
64
66
|
]);
|
|
65
67
|
const EXECUTABLE_GRAPH_TOOLS = new Set(["read", "write", "edit", "bash", "artifact_read"]);
|
|
68
|
+
const CONTROL_PLANE_TOOLS = new Set(["plan_graph", "task_run"]);
|
|
66
69
|
const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
67
70
|
const DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
|
|
68
71
|
const DEFAULT_KIMI_BASE_URL = "https://api.kimi.com/coding/v1";
|
|
@@ -78,11 +81,9 @@ const DEFAULT_NATIVE_TIMEOUT_MS = 43200000; // 12 hours
|
|
|
78
81
|
// via UFOO_UCODE_MAX_TOKENS (positive integer).
|
|
79
82
|
const DEFAULT_OPENAI_MAX_TOKENS = 131072;
|
|
80
83
|
const DEFAULT_ANTHROPIC_MAX_TOKENS = 64000;
|
|
81
|
-
// Extended thinking
|
|
82
|
-
//
|
|
83
|
-
// UFOO_UCODE_THINKING_BUDGET_TOKENS overrides
|
|
84
|
-
// disables thinking (the payload then omits the field entirely).
|
|
85
|
-
const DEFAULT_ANTHROPIC_THINKING_BUDGET_TOKENS = 10000;
|
|
84
|
+
// Extended thinking defaults live in thinkingLevels.js (medium = 10k).
|
|
85
|
+
// UFOO_UCODE_THINKING=off|low|medium|high|max selects a preset; numeric
|
|
86
|
+
// UFOO_UCODE_THINKING_BUDGET_TOKENS still overrides. 0 disables thinking.
|
|
86
87
|
// Prompt caching is GA on the current Messages API: cache_control blocks need
|
|
87
88
|
// no anthropic-beta header. Kept as a constant so the marker shape stays in
|
|
88
89
|
// one place (system block + last history message, 2 of the 4 allowed
|
|
@@ -116,14 +117,40 @@ function resolveMaxTokens(fallback) {
|
|
|
116
117
|
return normalizePositiveInt(process.env.UFOO_UCODE_MAX_TOKENS, fallback);
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
function resolveThinkingBudgetTokens() {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
function resolveThinkingBudgetTokens(options = {}) {
|
|
121
|
+
const { resolveThinkingFromEnvAndConfig } = require("./thinkingLevels");
|
|
122
|
+
const { loadGlobalUcodeConfig } = require("../config");
|
|
123
|
+
let configLevel = String(options.configLevel || "").trim();
|
|
124
|
+
if (!configLevel) {
|
|
125
|
+
try {
|
|
126
|
+
configLevel = String((loadGlobalUcodeConfig() || {}).ucodeThinking || "").trim();
|
|
127
|
+
} catch {
|
|
128
|
+
configLevel = "";
|
|
129
|
+
}
|
|
123
130
|
}
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
const resolved = resolveThinkingFromEnvAndConfig({
|
|
132
|
+
env: options.env || process.env,
|
|
133
|
+
configLevel,
|
|
134
|
+
});
|
|
135
|
+
return resolved.budgetTokens;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function resolveReasoningEffort(options = {}) {
|
|
139
|
+
const { resolveThinkingFromEnvAndConfig } = require("./thinkingLevels");
|
|
140
|
+
const { loadGlobalUcodeConfig } = require("../config");
|
|
141
|
+
let configLevel = String(options.configLevel || "").trim();
|
|
142
|
+
if (!configLevel) {
|
|
143
|
+
try {
|
|
144
|
+
configLevel = String((loadGlobalUcodeConfig() || {}).ucodeThinking || "").trim();
|
|
145
|
+
} catch {
|
|
146
|
+
configLevel = "";
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const resolved = resolveThinkingFromEnvAndConfig({
|
|
150
|
+
env: options.env || process.env,
|
|
151
|
+
configLevel,
|
|
152
|
+
});
|
|
153
|
+
return resolved.reasoningEffort || "";
|
|
127
154
|
}
|
|
128
155
|
|
|
129
156
|
function toUsageInt(value) {
|
|
@@ -446,12 +473,13 @@ function buildCoreToolSpecs() {
|
|
|
446
473
|
function: {
|
|
447
474
|
name: "plan_graph",
|
|
448
475
|
description: [
|
|
449
|
-
"Manage the persistent Plan Graph and
|
|
450
|
-
"
|
|
451
|
-
"
|
|
476
|
+
"Manage the persistent Plan Graph and graph-bound TaskRuns.",
|
|
477
|
+
"TaskRuns are orthogonal to Plan Mode; for a standalone TaskRun without a plan, use `task_run` instead.",
|
|
478
|
+
"Use create, patch, inspect, or cancel_graph for graph operations, and control for graph-bound TaskRun lifecycle.",
|
|
479
|
+
"`control.start_task` starts a graph `task_loop` asynchronously and returns immediately.",
|
|
452
480
|
"Use `inline_llm` for work handled by the current graph owner,",
|
|
453
481
|
"`expand` for tasks that must be lowered into child nodes,",
|
|
454
|
-
"and `task_loop` for asynchronous work in an independent TaskLoop.",
|
|
482
|
+
"and `task_loop` for asynchronous work in an independent TaskLoop attached to a plan node.",
|
|
455
483
|
"Do not call `plan_graph` together with data-plane tools in the same assistant turn.",
|
|
456
484
|
].join(" "),
|
|
457
485
|
parameters: {
|
|
@@ -517,6 +545,63 @@ function buildCoreToolSpecs() {
|
|
|
517
545
|
},
|
|
518
546
|
},
|
|
519
547
|
},
|
|
548
|
+
{
|
|
549
|
+
type: "function",
|
|
550
|
+
function: {
|
|
551
|
+
name: "task_run",
|
|
552
|
+
description: [
|
|
553
|
+
"Start, inspect, cancel, fail, or complete a TaskRun.",
|
|
554
|
+
"TaskRuns are orthogonal to Plan Mode and do not require a plan_graph.",
|
|
555
|
+
"Use operation=start with an objective for a standalone single-point TaskRun; it returns immediately.",
|
|
556
|
+
"On complex multi-goal work, decompose into concrete objectives and start one or more TaskRuns.",
|
|
557
|
+
"Use plan_graph control.start_task only when the TaskRun is attached to a plan_graph task_loop node.",
|
|
558
|
+
"Do not call `task_run` together with data-plane tools in the same assistant turn.",
|
|
559
|
+
].join(" "),
|
|
560
|
+
parameters: {
|
|
561
|
+
type: "object",
|
|
562
|
+
properties: {
|
|
563
|
+
operation: {
|
|
564
|
+
type: "string",
|
|
565
|
+
enum: ["start", "cancel", "fail", "complete", "inspect"],
|
|
566
|
+
description: [
|
|
567
|
+
"start creates a standalone TaskRun from objective;",
|
|
568
|
+
"cancel/fail/complete/inspect address an existing taskRunId",
|
|
569
|
+
"(cancel/fail may also use nodeId for graph-bound runs).",
|
|
570
|
+
].join(" "),
|
|
571
|
+
},
|
|
572
|
+
objective: {
|
|
573
|
+
type: "string",
|
|
574
|
+
description: "Required for start: concrete TaskRun objective.",
|
|
575
|
+
},
|
|
576
|
+
title: {
|
|
577
|
+
type: "string",
|
|
578
|
+
description: "Optional short title for start.",
|
|
579
|
+
},
|
|
580
|
+
taskRunId: {
|
|
581
|
+
type: "string",
|
|
582
|
+
description: "TaskRun id for cancel, fail, complete, or inspect.",
|
|
583
|
+
},
|
|
584
|
+
nodeId: {
|
|
585
|
+
type: "string",
|
|
586
|
+
description: "Optional graph node id for cancel/fail of a graph-bound TaskRun.",
|
|
587
|
+
},
|
|
588
|
+
reason: {
|
|
589
|
+
type: "string",
|
|
590
|
+
description: "Optional reason for cancel or fail.",
|
|
591
|
+
},
|
|
592
|
+
result: {
|
|
593
|
+
type: "object",
|
|
594
|
+
description: "Optional result payload for complete (TaskLoop owner).",
|
|
595
|
+
},
|
|
596
|
+
commandId: {
|
|
597
|
+
type: "string",
|
|
598
|
+
description: "Optional idempotency key for explicit replay.",
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
required: ["operation"],
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
},
|
|
520
605
|
{
|
|
521
606
|
type: "function",
|
|
522
607
|
function: {
|
|
@@ -772,6 +857,51 @@ function runCoreTool({
|
|
|
772
857
|
};
|
|
773
858
|
}
|
|
774
859
|
|
|
860
|
+
if (normalizedTool === "task_run") {
|
|
861
|
+
const state = executionState && typeof executionState === "object"
|
|
862
|
+
? executionState
|
|
863
|
+
: emptyExecutionState();
|
|
864
|
+
const result = runTaskRunTool(safeArgs, {
|
|
865
|
+
executionState: state,
|
|
866
|
+
runTool: ({ node, args: nestedArgs, tool: nestedTool, stepId }) => {
|
|
867
|
+
const nested = runCoreTool({
|
|
868
|
+
tool: nestedTool,
|
|
869
|
+
args: nestedArgs,
|
|
870
|
+
workspaceRoot,
|
|
871
|
+
onToolEvent,
|
|
872
|
+
sessionId,
|
|
873
|
+
onArtifactPersisted,
|
|
874
|
+
executionState: state,
|
|
875
|
+
origin: {
|
|
876
|
+
kind: "task_run",
|
|
877
|
+
taskRunId: String(safeArgs.taskRunId || ""),
|
|
878
|
+
nodeId: stepId || (node && node.id) || "",
|
|
879
|
+
attempt: Number(node && node.attempt) || 0,
|
|
880
|
+
},
|
|
881
|
+
});
|
|
882
|
+
return nested;
|
|
883
|
+
},
|
|
884
|
+
});
|
|
885
|
+
const ok = result.ok !== false && result.status !== "rejected";
|
|
886
|
+
emitToolEvent(onToolEvent, {
|
|
887
|
+
tool: "task_run",
|
|
888
|
+
phase: ok ? "end" : "error",
|
|
889
|
+
args: safeArgs,
|
|
890
|
+
result,
|
|
891
|
+
error: ok
|
|
892
|
+
? ""
|
|
893
|
+
: (Array.isArray(result.errors)
|
|
894
|
+
? result.errors.map((e) => e.message || e.code).join("; ")
|
|
895
|
+
: (result.error || "task_run rejected")),
|
|
896
|
+
origin,
|
|
897
|
+
});
|
|
898
|
+
return {
|
|
899
|
+
...result,
|
|
900
|
+
ok,
|
|
901
|
+
executionState: result.executionState || state,
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
775
905
|
if (normalizedTool === "ask_user") {
|
|
776
906
|
const state = executionState && typeof executionState === "object"
|
|
777
907
|
? executionState
|
|
@@ -970,6 +1100,12 @@ async function runOpenAiLikeTurn({
|
|
|
970
1100
|
// Kimi k3 rejects any temperature other than 1.
|
|
971
1101
|
temperature: normalizeProvider(provider) === "kimi" ? 1 : 0,
|
|
972
1102
|
};
|
|
1103
|
+
const reasoningEffort = resolveReasoningEffort();
|
|
1104
|
+
if (reasoningEffort) {
|
|
1105
|
+
// OpenAI-compatible gateways that support reasoning models accept this;
|
|
1106
|
+
// unknown fields are typically ignored by plain chat models.
|
|
1107
|
+
payload.reasoning_effort = reasoningEffort;
|
|
1108
|
+
}
|
|
973
1109
|
|
|
974
1110
|
const headers = {
|
|
975
1111
|
"content-type": "application/json",
|
|
@@ -1715,16 +1851,16 @@ async function runNativeLoop({
|
|
|
1715
1851
|
await withFaultPoint("before_tool_exec", () => {});
|
|
1716
1852
|
|
|
1717
1853
|
const callNames = pendingCalls.map((call) => String(call.name || "").trim().toLowerCase());
|
|
1718
|
-
const
|
|
1854
|
+
const hasControlPlane = callNames.some((name) => CONTROL_PLANE_TOOLS.has(name));
|
|
1719
1855
|
const hasAskUser = callNames.includes("ask_user");
|
|
1720
1856
|
const hasDataTool = callNames.some((name) => EXECUTABLE_GRAPH_TOOLS.has(name));
|
|
1721
|
-
if (
|
|
1857
|
+
if (hasControlPlane && hasDataTool) {
|
|
1722
1858
|
// prepareToolCalls already appended the assistant tool_calls / tool_use
|
|
1723
1859
|
// message; every declared call must get a contiguous tool result via ledger.
|
|
1724
1860
|
const rejected = {
|
|
1725
1861
|
ok: false,
|
|
1726
1862
|
status: "rejected",
|
|
1727
|
-
error: "Do not mix plan_graph with data-plane tools in the same turn",
|
|
1863
|
+
error: "Do not mix plan_graph/task_run with data-plane tools in the same turn",
|
|
1728
1864
|
code: "MIXED_PLAN_AND_DATA_TOOLS",
|
|
1729
1865
|
};
|
|
1730
1866
|
for (const pending of pendingCalls) {
|
|
@@ -2078,6 +2214,8 @@ module.exports = {
|
|
|
2078
2214
|
resolveCompletionUrl,
|
|
2079
2215
|
resolveAnthropicMessagesUrl,
|
|
2080
2216
|
resolveTransport,
|
|
2217
|
+
resolveThinkingBudgetTokens,
|
|
2218
|
+
resolveReasoningEffort,
|
|
2081
2219
|
buildCoreToolSpecs,
|
|
2082
2220
|
buildAnthropicToolSpecs,
|
|
2083
2221
|
};
|
|
@@ -78,7 +78,7 @@ function validateDeclaredBatch(ledger = null, {
|
|
|
78
78
|
|
|
79
79
|
const names = calls.map((c) => c.name);
|
|
80
80
|
const hasAskUser = names.includes("ask_user");
|
|
81
|
-
const
|
|
81
|
+
const hasControlPlane = names.includes("plan_graph") || names.includes("task_run");
|
|
82
82
|
const dataSet = dataPlaneTools instanceof Set
|
|
83
83
|
? dataPlaneTools
|
|
84
84
|
: new Set(["read", "write", "edit", "bash", "artifact_read"]);
|
|
@@ -90,10 +90,10 @@ function validateDeclaredBatch(ledger = null, {
|
|
|
90
90
|
message: "ask_user must be the only tool call in the turn",
|
|
91
91
|
});
|
|
92
92
|
}
|
|
93
|
-
if (rejectPlanWithData &&
|
|
93
|
+
if (rejectPlanWithData && hasControlPlane && hasData) {
|
|
94
94
|
errors.push({
|
|
95
95
|
code: "MIXED_PLAN_AND_DATA_TOOLS",
|
|
96
|
-
message: "Do not mix plan_graph with data-plane tools in the same turn",
|
|
96
|
+
message: "Do not mix plan_graph/task_run with data-plane tools in the same turn",
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
99
|
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Live provider model catalog via the OpenAI-compatible / Anthropic models route.
|
|
5
|
+
*
|
|
6
|
+
* Used by /model suggestions and settings validation so ucode only offers
|
|
7
|
+
* (and preferably accepts) models the configured endpoint actually lists.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 8000;
|
|
12
|
+
const CACHE_TTL_MS = 60_000;
|
|
13
|
+
|
|
14
|
+
/** @type {Map<string, { at: number, result: object }>} */
|
|
15
|
+
const modelsCache = new Map();
|
|
16
|
+
|
|
17
|
+
function clipText(value = "", maxChars = 400) {
|
|
18
|
+
const text = String(value || "");
|
|
19
|
+
if (text.length <= maxChars) return text;
|
|
20
|
+
return `${text.slice(0, maxChars)}…`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveOpenAiModelsUrl(baseUrl = "") {
|
|
24
|
+
const raw = String(baseUrl || "").trim();
|
|
25
|
+
if (!raw) return "";
|
|
26
|
+
const normalized = raw.replace(/\/+$/, "");
|
|
27
|
+
if (/\/models$/i.test(normalized)) return normalized;
|
|
28
|
+
if (/\/chat\/completions$/i.test(normalized)) {
|
|
29
|
+
return normalized.replace(/\/chat\/completions$/i, "/models");
|
|
30
|
+
}
|
|
31
|
+
if (/\/v1$/i.test(normalized)) return `${normalized}/models`;
|
|
32
|
+
if (/\/api$/i.test(normalized)) return `${normalized}/v1/models`;
|
|
33
|
+
return `${normalized}/models`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveAnthropicModelsUrl(baseUrl = "") {
|
|
37
|
+
const raw = String(baseUrl || "").trim() || DEFAULT_ANTHROPIC_BASE_URL;
|
|
38
|
+
const normalized = raw.replace(/\/+$/, "");
|
|
39
|
+
if (/\/models$/i.test(normalized)) return normalized;
|
|
40
|
+
if (/\/messages$/i.test(normalized)) {
|
|
41
|
+
return normalized.replace(/\/messages$/i, "/models");
|
|
42
|
+
}
|
|
43
|
+
if (/\/v1$/i.test(normalized)) return `${normalized}/models`;
|
|
44
|
+
if (/\/api$/i.test(normalized)) return `${normalized}/v1/models`;
|
|
45
|
+
return `${normalized}/models`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveModelsUrl({ transport = "", baseUrl = "" } = {}) {
|
|
49
|
+
if (String(transport || "") === "anthropic-messages") {
|
|
50
|
+
return resolveAnthropicModelsUrl(baseUrl);
|
|
51
|
+
}
|
|
52
|
+
return resolveOpenAiModelsUrl(baseUrl);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function cacheKey({ transport = "", baseUrl = "", apiKey = "", provider = "" } = {}) {
|
|
56
|
+
const keyTail = apiKey ? String(apiKey).slice(-8) : "";
|
|
57
|
+
return `${provider}|${transport}|${baseUrl}|${keyTail}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function extractModelIds(payload) {
|
|
61
|
+
const ids = [];
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
const push = (value) => {
|
|
64
|
+
const id = String(value || "").trim();
|
|
65
|
+
if (!id || seen.has(id)) return;
|
|
66
|
+
seen.add(id);
|
|
67
|
+
ids.push(id);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (!payload || typeof payload !== "object") return ids;
|
|
71
|
+
|
|
72
|
+
if (Array.isArray(payload.data)) {
|
|
73
|
+
for (const item of payload.data) {
|
|
74
|
+
if (!item) continue;
|
|
75
|
+
if (typeof item === "string") push(item);
|
|
76
|
+
else push(item.id || item.model || item.name);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (Array.isArray(payload.models)) {
|
|
81
|
+
for (const item of payload.models) {
|
|
82
|
+
if (!item) continue;
|
|
83
|
+
if (typeof item === "string") push(item);
|
|
84
|
+
else push(item.id || item.model || item.name);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return ids;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildListHeaders({ transport = "", apiKey = "" } = {}) {
|
|
92
|
+
const headers = {
|
|
93
|
+
Accept: "application/json",
|
|
94
|
+
};
|
|
95
|
+
const key = String(apiKey || "").trim();
|
|
96
|
+
if (!key) return headers;
|
|
97
|
+
|
|
98
|
+
if (String(transport || "") === "anthropic-messages") {
|
|
99
|
+
headers["x-api-key"] = key;
|
|
100
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
101
|
+
} else {
|
|
102
|
+
headers.Authorization = `Bearer ${key}`;
|
|
103
|
+
}
|
|
104
|
+
return headers;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Fetch the provider's models catalog.
|
|
109
|
+
* @returns {Promise<{
|
|
110
|
+
* ok: boolean,
|
|
111
|
+
* models: string[],
|
|
112
|
+
* url: string,
|
|
113
|
+
* error: string,
|
|
114
|
+
* status: number,
|
|
115
|
+
* cached: boolean,
|
|
116
|
+
* }>}
|
|
117
|
+
*/
|
|
118
|
+
async function listProviderModels(options = {}) {
|
|
119
|
+
const transport = String(options.transport || "openai-chat").trim() || "openai-chat";
|
|
120
|
+
const baseUrl = String(options.baseUrl || "").trim();
|
|
121
|
+
const apiKey = String(options.apiKey || "").trim();
|
|
122
|
+
const provider = String(options.provider || "").trim();
|
|
123
|
+
const timeoutMs = Math.max(1000, Number(options.timeoutMs) || DEFAULT_TIMEOUT_MS);
|
|
124
|
+
const fetchImpl = typeof options.fetchImpl === "function" ? options.fetchImpl : fetch;
|
|
125
|
+
const skipCache = options.skipCache === true;
|
|
126
|
+
const url = resolveModelsUrl({ transport, baseUrl });
|
|
127
|
+
|
|
128
|
+
if (!url) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
models: [],
|
|
132
|
+
url: "",
|
|
133
|
+
error: "models url unavailable (set ucode base url)",
|
|
134
|
+
status: 0,
|
|
135
|
+
cached: false,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const key = cacheKey({ transport, baseUrl, apiKey, provider });
|
|
140
|
+
if (!skipCache) {
|
|
141
|
+
const hit = modelsCache.get(key);
|
|
142
|
+
if (hit && (Date.now() - hit.at) < CACHE_TTL_MS) {
|
|
143
|
+
return { ...hit.result, cached: true };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
148
|
+
const timer = controller
|
|
149
|
+
? setTimeout(() => {
|
|
150
|
+
try { controller.abort(); } catch { /* ignore */ }
|
|
151
|
+
}, timeoutMs)
|
|
152
|
+
: null;
|
|
153
|
+
if (timer && typeof timer.unref === "function") timer.unref();
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const response = await fetchImpl(url, {
|
|
157
|
+
method: "GET",
|
|
158
|
+
headers: buildListHeaders({ transport, apiKey }),
|
|
159
|
+
signal: controller ? controller.signal : undefined,
|
|
160
|
+
});
|
|
161
|
+
const status = Number(response && response.status) || 0;
|
|
162
|
+
const bodyText = await response.text().catch(() => "");
|
|
163
|
+
let payload = null;
|
|
164
|
+
try {
|
|
165
|
+
payload = bodyText ? JSON.parse(bodyText) : null;
|
|
166
|
+
} catch {
|
|
167
|
+
payload = null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
const result = {
|
|
172
|
+
ok: false,
|
|
173
|
+
models: [],
|
|
174
|
+
url,
|
|
175
|
+
error: `models route failed (${status}): ${clipText(bodyText || response.statusText || "unknown")}`,
|
|
176
|
+
status,
|
|
177
|
+
cached: false,
|
|
178
|
+
};
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const models = extractModelIds(payload);
|
|
183
|
+
const result = {
|
|
184
|
+
ok: true,
|
|
185
|
+
models,
|
|
186
|
+
url,
|
|
187
|
+
error: models.length === 0 ? "models route returned an empty catalog" : "",
|
|
188
|
+
status,
|
|
189
|
+
cached: false,
|
|
190
|
+
};
|
|
191
|
+
// Cache successful responses even when empty — avoids hammering a broken gateway.
|
|
192
|
+
modelsCache.set(key, { at: Date.now(), result: { ...result, cached: false } });
|
|
193
|
+
return result;
|
|
194
|
+
} catch (err) {
|
|
195
|
+
const message = err && err.name === "AbortError"
|
|
196
|
+
? `models route timed out after ${timeoutMs}ms`
|
|
197
|
+
: (err && err.message ? err.message : String(err || "models route failed"));
|
|
198
|
+
return {
|
|
199
|
+
ok: false,
|
|
200
|
+
models: [],
|
|
201
|
+
url,
|
|
202
|
+
error: message,
|
|
203
|
+
status: 0,
|
|
204
|
+
cached: false,
|
|
205
|
+
};
|
|
206
|
+
} finally {
|
|
207
|
+
if (timer) clearTimeout(timer);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function modelSupported(model = "", models = []) {
|
|
212
|
+
const needle = String(model || "").trim();
|
|
213
|
+
if (!needle) return false;
|
|
214
|
+
const list = Array.isArray(models) ? models : [];
|
|
215
|
+
return list.some((id) => String(id || "").trim() === needle);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Validate a model id against the live catalog.
|
|
220
|
+
* Soft mode: when the catalog cannot be fetched, allow with a warning.
|
|
221
|
+
*/
|
|
222
|
+
async function confirmModelSupported(options = {}) {
|
|
223
|
+
const model = String(options.model || "").trim();
|
|
224
|
+
if (!model) {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
allowed: false,
|
|
228
|
+
model: "",
|
|
229
|
+
models: [],
|
|
230
|
+
error: "model id is empty",
|
|
231
|
+
warning: "",
|
|
232
|
+
catalog: null,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const catalog = await listProviderModels(options);
|
|
237
|
+
if (!catalog.ok) {
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
allowed: options.strict === true ? false : true,
|
|
241
|
+
model,
|
|
242
|
+
models: [],
|
|
243
|
+
error: catalog.error || "models route unavailable",
|
|
244
|
+
warning: options.strict === true
|
|
245
|
+
? ""
|
|
246
|
+
: `could not confirm model via models route (${catalog.error || "unavailable"}); accepting ${model}`,
|
|
247
|
+
catalog,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (catalog.models.length === 0) {
|
|
252
|
+
return {
|
|
253
|
+
ok: false,
|
|
254
|
+
allowed: options.strict === true ? false : true,
|
|
255
|
+
model,
|
|
256
|
+
models: [],
|
|
257
|
+
error: catalog.error || "empty models catalog",
|
|
258
|
+
warning: options.strict === true
|
|
259
|
+
? ""
|
|
260
|
+
: `models route returned no models; accepting ${model}`,
|
|
261
|
+
catalog,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!modelSupported(model, catalog.models)) {
|
|
266
|
+
const sample = catalog.models.slice(0, 8).join(", ");
|
|
267
|
+
const more = catalog.models.length > 8 ? ` (+${catalog.models.length - 8} more)` : "";
|
|
268
|
+
return {
|
|
269
|
+
ok: false,
|
|
270
|
+
allowed: false,
|
|
271
|
+
model,
|
|
272
|
+
models: catalog.models,
|
|
273
|
+
error: `model "${model}" is not in the provider catalog${sample ? ` (available: ${sample}${more})` : ""}`,
|
|
274
|
+
warning: "",
|
|
275
|
+
catalog,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
ok: true,
|
|
281
|
+
allowed: true,
|
|
282
|
+
model,
|
|
283
|
+
models: catalog.models,
|
|
284
|
+
error: "",
|
|
285
|
+
warning: "",
|
|
286
|
+
catalog,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function clearModelsCache() {
|
|
291
|
+
modelsCache.clear();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = {
|
|
295
|
+
resolveOpenAiModelsUrl,
|
|
296
|
+
resolveAnthropicModelsUrl,
|
|
297
|
+
resolveModelsUrl,
|
|
298
|
+
listProviderModels,
|
|
299
|
+
confirmModelSupported,
|
|
300
|
+
modelSupported,
|
|
301
|
+
extractModelIds,
|
|
302
|
+
clearModelsCache,
|
|
303
|
+
CACHE_TTL_MS,
|
|
304
|
+
};
|
package/src/code/repl.js
CHANGED
|
@@ -113,21 +113,24 @@ function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
|
|
|
113
113
|
}
|
|
114
114
|
const modelMatch = text.match(/^(?:\/model|model)(?:\s+(.*))?$/i);
|
|
115
115
|
if (modelMatch) {
|
|
116
|
-
const
|
|
117
|
-
if (!
|
|
116
|
+
const rest = String(modelMatch[1] || "").trim();
|
|
117
|
+
if (!rest) {
|
|
118
118
|
return { kind: "model", action: "show" };
|
|
119
119
|
}
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
const parts = rest.split(/\s+/).filter(Boolean);
|
|
121
|
+
const modelId = parts[0] || "";
|
|
122
|
+
const thinking = parts[1] || "";
|
|
123
|
+
if (!modelId || parts.length > 2) {
|
|
122
124
|
return {
|
|
123
125
|
kind: "error",
|
|
124
|
-
output: "usage: /model [model-id]",
|
|
126
|
+
output: "usage: /model [model-id] [off|low|medium|high|max]",
|
|
125
127
|
};
|
|
126
128
|
}
|
|
127
129
|
return {
|
|
128
130
|
kind: "model",
|
|
129
131
|
action: "set",
|
|
130
|
-
model:
|
|
132
|
+
model: modelId,
|
|
133
|
+
thinking,
|
|
131
134
|
};
|
|
132
135
|
}
|
|
133
136
|
const planMatch = text.match(/^(?:\/plan|plan)(?:\s+(.*))?$/i);
|
|
@@ -291,10 +294,29 @@ async function runUcodeCoreAgent({
|
|
|
291
294
|
provider,
|
|
292
295
|
model,
|
|
293
296
|
});
|
|
297
|
+
const {
|
|
298
|
+
currentThinkingLevel,
|
|
299
|
+
} = require("./modelCommand");
|
|
300
|
+
const {
|
|
301
|
+
resolveThinkingFromEnvAndConfig,
|
|
302
|
+
applyThinkingLevelToEnv,
|
|
303
|
+
} = require("./thinkingLevels");
|
|
304
|
+
const { loadGlobalUcodeConfig } = require("../config");
|
|
305
|
+
let initialThinking = "";
|
|
306
|
+
try {
|
|
307
|
+
initialThinking = String((loadGlobalUcodeConfig() || {}).ucodeThinking || "").trim();
|
|
308
|
+
} catch {
|
|
309
|
+
initialThinking = "";
|
|
310
|
+
}
|
|
311
|
+
const thinkingResolved = resolveThinkingFromEnvAndConfig({
|
|
312
|
+
env: process.env,
|
|
313
|
+
configLevel: initialThinking,
|
|
314
|
+
});
|
|
294
315
|
const state = {
|
|
295
316
|
workspaceRoot: resolvedWorkspaceRoot,
|
|
296
317
|
provider: resolvedUcode.provider,
|
|
297
318
|
model: resolvedUcode.model,
|
|
319
|
+
thinking: currentThinkingLevel({ thinking: initialThinking }),
|
|
298
320
|
engine: "ufoo-core",
|
|
299
321
|
context: buildNlContext({
|
|
300
322
|
appendSystemPrompt,
|
|
@@ -308,6 +330,10 @@ async function runUcodeCoreAgent({
|
|
|
308
330
|
timeoutMs: resolveNlTaskTimeoutMs(Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : NaN),
|
|
309
331
|
jsonOutput,
|
|
310
332
|
};
|
|
333
|
+
// Named levels sync into env; leave an explicit numeric budget override alone.
|
|
334
|
+
if (thinkingResolved.source !== "env-budget") {
|
|
335
|
+
applyThinkingLevelToEnv(state.thinking, process.env);
|
|
336
|
+
}
|
|
311
337
|
persistSessionState(state);
|
|
312
338
|
|
|
313
339
|
if (shouldUseUcodeTui({
|
|
@@ -466,7 +492,9 @@ async function runUcodeCoreAgent({
|
|
|
466
492
|
}
|
|
467
493
|
}
|
|
468
494
|
if (result.kind === "model") {
|
|
469
|
-
const applied = applyUcodeModelCommand(state, result
|
|
495
|
+
const applied = await applyUcodeModelCommand(state, result, {
|
|
496
|
+
workspaceRoot: runtimeWorkspace,
|
|
497
|
+
});
|
|
470
498
|
stdout.write(`${applied.output}\n`);
|
|
471
499
|
if (applied.ok && result.action === "set") {
|
|
472
500
|
persistSessionState(state);
|
|
@@ -770,4 +798,5 @@ module.exports = {
|
|
|
770
798
|
applyUcodeModelCommand,
|
|
771
799
|
applyUcodePlanCommand,
|
|
772
800
|
suggestUcodeModels,
|
|
773
|
-
|
|
801
|
+
suggestUcodeThinkingLevels: require("./modelCommand").suggestUcodeThinkingLevels,
|
|
802
|
+
};
|