nolo-cli 0.1.33 → 0.1.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.
Files changed (56) hide show
  1. package/README.md +4 -4
  2. package/agent-runtime/agentRecordConfig.ts +8 -0
  3. package/agent-runtime/hostAdapter.ts +9 -1
  4. package/agent-runtime/index.ts +10 -0
  5. package/agent-runtime/localLoop.ts +49 -2
  6. package/agent-runtime/localToolPolicy.ts +70 -21
  7. package/agent-runtime/openAiCompatibleProvider.ts +7 -3
  8. package/agent-runtime/openAiCompatibleProviderConfig.ts +15 -26
  9. package/agent-runtime/platformChatProvider.ts +48 -71
  10. package/agent-runtime/providerResolution.ts +244 -0
  11. package/agent-runtime/runtimeToolPolicy.ts +33 -0
  12. package/agent-runtime/shellCommandPolicy.ts +120 -0
  13. package/agentInternalCommandEntries.ts +2 -0
  14. package/agentListCommands.ts +2 -2
  15. package/agentPullCommand.ts +6 -11
  16. package/agentRecordCommands.ts +87 -2
  17. package/agentRecordHelpers.ts +87 -4
  18. package/agentRuntimeLocal.ts +3 -0
  19. package/ai/tools/toolNameAliases.ts +70 -0
  20. package/cli/agentInternalCommandEntries.ts +2 -0
  21. package/cli/agentListCommands.ts +2 -2
  22. package/cli/agentPullCommand.ts +6 -11
  23. package/cli/agentRecordCommands.ts +87 -2
  24. package/cli/agentRecordHelpers.ts +87 -4
  25. package/cli/agentRuntimeLocal.ts +3 -0
  26. package/cli/cliCommandDispatch.ts +2 -2
  27. package/cli/cliEnvHelpers.ts +80 -12
  28. package/cli/client/agentRun.ts +1 -0
  29. package/cli/client/localRuntimeAdapter.ts +44 -7
  30. package/cli/commandRegistry.ts +2 -2
  31. package/cli/machineCommands.ts +1 -1
  32. package/cli/machineWsRunDispatch.ts +29 -9
  33. package/cli/offlineMarxistsAgentCommand.ts +1 -1
  34. package/cli/runtimeDoctorCommands.ts +10 -4
  35. package/cli/tableCommands.ts +1 -1
  36. package/cli/taskRunCommand.ts +1 -1
  37. package/cliCommandDispatch.ts +2 -2
  38. package/cliEnvHelpers.ts +80 -12
  39. package/client/agentRun.ts +1 -0
  40. package/client/localProviderResolver.test.ts +26 -2
  41. package/client/localRuntimeAdapter.test.ts +287 -3
  42. package/client/localRuntimeAdapter.ts +44 -7
  43. package/commandRegistry.ts +2 -2
  44. package/database/server/authorityStoreTypes.ts +31 -0
  45. package/database/server/db.ts +15 -34
  46. package/database/server/legacyServerDb.ts +58 -0
  47. package/database/server/levelAuthorityStore.ts +75 -0
  48. package/database/server/memoryAuthorityStore.ts +133 -0
  49. package/database/server/serverStoreFactory.ts +118 -0
  50. package/machineCommands.ts +1 -1
  51. package/machineWsRunDispatch.ts +29 -9
  52. package/offlineMarxistsAgentCommand.ts +1 -1
  53. package/package.json +10 -2
  54. package/runtimeDoctorCommands.ts +10 -4
  55. package/tableCommands.ts +1 -1
  56. package/taskRunCommand.ts +1 -1
package/README.md CHANGED
@@ -90,7 +90,7 @@ nolo chat
90
90
  nolo connect
91
91
  nolo connect --watch
92
92
  nolo connect --daemon
93
- nolo daemon --server-url https://api.nolo.chat --api-key sk_machine_xxx
93
+ nolo daemon --server-url https://api.nolo.chat --machine-key sk_machine_xxx
94
94
  nolo machine status
95
95
  nolo run "summarize my latest agent dialogs"
96
96
  nolo doc create --title "Trip Notes" --body "hello" --sync local,us --dry-run
@@ -116,7 +116,7 @@ nolo connect # send one machine heartbeat
116
116
  nolo connect --watch # keep this terminal process online with periodic heartbeats
117
117
  nolo connect --ws # keep a live connector websocket for bound agent runs
118
118
  nolo connect --daemon # start connect --ws silently in the background
119
- nolo daemon --server-url https://api.nolo.chat --api-key sk_machine_xxx
119
+ nolo daemon --server-url https://api.nolo.chat --machine-key sk_machine_xxx
120
120
  nolo machine status # list machines registered to the current profile
121
121
  ```
122
122
 
@@ -126,13 +126,13 @@ They do not expose shell, file-write, or raw local LLM endpoints.
126
126
  Run a Slock-style connector command:
127
127
 
128
128
  ```bash
129
- nolo daemon --server-url https://api.nolo.chat --api-key sk_machine_xxx
129
+ nolo daemon --server-url https://api.nolo.chat --machine-key sk_machine_xxx
130
130
  ```
131
131
 
132
132
  Future published package shape:
133
133
 
134
134
  ```bash
135
- npx @nolo/daemon@latest --server-url https://api.nolo.chat --api-key sk_machine_xxx
135
+ npx @nolo/daemon@latest --server-url https://api.nolo.chat --machine-key sk_machine_xxx
136
136
  ```
137
137
 
138
138
  The daemon registers this computer, reports local CLI capabilities, keeps a live
@@ -64,6 +64,10 @@ export function resolveAgentRuntimeConfigFromRecord(
64
64
  const apiSource = stringField(record, "apiSource");
65
65
  const cliProvider = stringField(record, "cliProvider");
66
66
  const customProviderUrl = stringField(record, "customProviderUrl");
67
+ const apiKey = stringField(record, "apiKey");
68
+ const apiKeyHeader = stringField(record, "apiKeyHeader");
69
+ const apiKeyFromAgentKey = stringField(record, "apiKeyFromAgentKey");
70
+ const useServerProxy = record.useServerProxy === true;
67
71
  const provider = stringField(record, "provider") ?? stringField(record, "apiSource");
68
72
  const toolNames = resolveAgentRuntimeToolNames(record);
69
73
  const temperature = numberField(record, "temperature");
@@ -87,6 +91,10 @@ export function resolveAgentRuntimeConfigFromRecord(
87
91
  ...(apiSource ? { apiSource } : {}),
88
92
  ...(cliProvider ? { cliProvider } : {}),
89
93
  ...(customProviderUrl ? { customProviderUrl } : {}),
94
+ ...(apiKey ? { apiKey } : {}),
95
+ ...(apiKeyHeader ? { apiKeyHeader } : {}),
96
+ ...(apiKeyFromAgentKey ? { apiKeyFromAgentKey } : {}),
97
+ ...(useServerProxy ? { useServerProxy } : {}),
90
98
  ...(toolNames ? { toolNames } : {}),
91
99
  ...(temperature !== undefined ? { temperature } : {}),
92
100
  ...(topP !== undefined ? { top_p: topP } : {}),
@@ -14,6 +14,10 @@ export type AgentRuntimeAgentConfig = {
14
14
  apiSource?: string;
15
15
  cliProvider?: string;
16
16
  customProviderUrl?: string;
17
+ apiKey?: string;
18
+ apiKeyHeader?: string;
19
+ apiKeyFromAgentKey?: string;
20
+ useServerProxy?: boolean;
17
21
  toolNames?: string[];
18
22
  temperature?: number;
19
23
  top_p?: number;
@@ -30,13 +34,17 @@ export type AgentRuntimeAgentConfig = {
30
34
 
31
35
  export type AgentRuntimeProvider = {
32
36
  model: string;
33
- complete(messages: AgentRuntimeChatMessage[]): Promise<AgentRuntimeResult>;
37
+ complete(
38
+ messages: AgentRuntimeChatMessage[],
39
+ options?: { timeoutMs?: number }
40
+ ): Promise<AgentRuntimeResult>;
34
41
  };
35
42
 
36
43
  export type AgentRuntimeToolCallInput = {
37
44
  id: string;
38
45
  name: string;
39
46
  arguments: string;
47
+ userInput?: string;
40
48
  };
41
49
 
42
50
  export type AgentRuntimeToolResult = {
@@ -10,6 +10,11 @@ export {
10
10
  parseOpenAiCompatibleChatCompletionResponse,
11
11
  } from "./openAiCompatibleProvider";
12
12
  export { resolveOpenAiCompatibleProviderConfig } from "./openAiCompatibleProviderConfig";
13
+ export {
14
+ buildProviderAuthHeaders,
15
+ buildProviderExecutionPlan,
16
+ resolveAgentProviderMode,
17
+ } from "./providerResolution";
13
18
  export {
14
19
  buildPlatformChatCompletionRequest,
15
20
  canUsePlatformChatProvider,
@@ -45,9 +50,14 @@ export {
45
50
  executeLocalToolWithPolicy,
46
51
  resolveLocalToolPolicy,
47
52
  } from "./localToolPolicy";
53
+ export {
54
+ evaluateShellCommandPolicy,
55
+ isDestructiveShellCommand,
56
+ } from "./shellCommandPolicy";
48
57
  export {
49
58
  mergeAgentRuntimeToolPolicies,
50
59
  normalizeAgentRuntimeToolPolicy,
60
+ resolveRequestedRuntimeToolNames,
51
61
  resolveEffectiveRuntimeToolPolicy,
52
62
  } from "./runtimeToolPolicy";
53
63
  export {
@@ -13,6 +13,7 @@ export type LocalAgentTurnInput = {
13
13
  input: AgentRuntimeMessageContent;
14
14
  continueDialogId?: string;
15
15
  maxToolRounds?: number;
16
+ timeoutMs?: number;
16
17
  onToolEvent?: (event: LocalAgentToolEvent) => void;
17
18
  };
18
19
 
@@ -40,6 +41,29 @@ function formatToolExecutionError(args: {
40
41
  return `${args.toolName} failed: ${message}`;
41
42
  }
42
43
 
44
+ function formatStructuredToolExecutionError(args: {
45
+ toolName: string;
46
+ error: unknown;
47
+ }) {
48
+ if (!args.error || typeof args.error !== "object") return null;
49
+ const error = args.error as {
50
+ code?: unknown;
51
+ message?: unknown;
52
+ policy?: unknown;
53
+ };
54
+ if (typeof error.code !== "string") return null;
55
+ return JSON.stringify({
56
+ error: error.code,
57
+ message:
58
+ typeof error.message === "string"
59
+ ? error.message
60
+ : formatToolExecutionError(args),
61
+ ...(error.policy && typeof error.policy === "object"
62
+ ? { policy: error.policy }
63
+ : {}),
64
+ });
65
+ }
66
+
43
67
  function shouldReturnToolExecutionErrors(adapter: AgentRuntimeHostAdapter) {
44
68
  return adapter.capabilities.includes("local-tools");
45
69
  }
@@ -66,6 +90,16 @@ function buildMessages(args: {
66
90
  ];
67
91
  }
68
92
 
93
+ function extractUserInputText(content: AgentRuntimeMessageContent): string {
94
+ if (typeof content === "string") return content;
95
+ if (!Array.isArray(content)) return "";
96
+ return content
97
+ .map((part) => (part?.type === "text" ? part.text : ""))
98
+ .filter(Boolean)
99
+ .join("\n")
100
+ .trim();
101
+ }
102
+
69
103
  export async function runLocalAgentTurn(
70
104
  input: LocalAgentTurnInput
71
105
  ): Promise<LocalAgentTurnResult> {
@@ -92,10 +126,13 @@ export async function runLocalAgentTurn(
92
126
  });
93
127
  const provider = await input.adapter.resolveProvider(agentConfig);
94
128
  const maxToolRounds = input.maxToolRounds ?? DEFAULT_LOCAL_AGENT_MAX_TOOL_ROUNDS;
129
+ const userInputText = extractUserInputText(input.input);
95
130
  let toolCallCount = 0;
96
131
  let result: AgentRuntimeResult;
97
132
  for (let round = 0; round <= maxToolRounds; round += 1) {
98
- result = await provider.complete(messages);
133
+ result = await provider.complete(messages, {
134
+ ...(typeof input.timeoutMs === "number" ? { timeoutMs: input.timeoutMs } : {}),
135
+ });
99
136
  const toolCalls = result.tool_calls ?? [];
100
137
  if (toolCalls.length === 0) break;
101
138
  if (round === maxToolRounds) {
@@ -122,6 +159,7 @@ export async function runLocalAgentTurn(
122
159
  id: toolCall.id,
123
160
  name: toolName,
124
161
  arguments: toolCall.function.arguments,
162
+ ...(userInputText ? { userInput: userInputText } : {}),
125
163
  });
126
164
  emitToolEvent(input, {
127
165
  type: "tool-result",
@@ -140,11 +178,20 @@ export async function runLocalAgentTurn(
140
178
  message: error instanceof Error ? error.message : String(error),
141
179
  });
142
180
  toolResult = {
143
- content: formatToolExecutionError({ toolName, error }),
181
+ content:
182
+ formatStructuredToolExecutionError({ toolName, error }) ??
183
+ formatToolExecutionError({ toolName, error }),
144
184
  metadata: {
145
185
  error: true,
146
186
  toolName,
147
187
  message: error instanceof Error ? error.message : String(error),
188
+ ...(
189
+ error &&
190
+ typeof error === "object" &&
191
+ typeof (error as { code?: unknown }).code === "string"
192
+ ? { code: (error as { code: string }).code }
193
+ : {}
194
+ ),
148
195
  },
149
196
  };
150
197
  }
@@ -2,6 +2,8 @@ import type {
2
2
  AgentRuntimeToolCallInput,
3
3
  AgentRuntimeToolResult,
4
4
  } from "./types";
5
+ import { canonicalizeToolName } from "../ai/tools/toolNameAliases";
6
+ import { evaluateShellCommandPolicy } from "./shellCommandPolicy";
5
7
 
6
8
  type EnvLike = Record<string, string | undefined>;
7
9
 
@@ -48,6 +50,22 @@ function parseToolAllowlist(value: string | undefined) {
48
50
  .filter(Boolean);
49
51
  }
50
52
 
53
+ function normalizeLocalToolName(toolName: string) {
54
+ return canonicalizeToolName(
55
+ String(toolName ?? "").replace(/^functions\./, "").trim(),
56
+ );
57
+ }
58
+
59
+ function parseShellCommandPayload(rawArguments: string) {
60
+ try {
61
+ const parsed = JSON.parse(rawArguments || "{}");
62
+ if (!parsed || typeof parsed !== "object") return {};
63
+ return parsed as { command?: unknown; cmd?: unknown; input?: unknown };
64
+ } catch {
65
+ return {};
66
+ }
67
+ }
68
+
51
69
  function isLocalShellModeEnabled(env: EnvLike) {
52
70
  return ["worktree", "dangerous", "1", "true"].includes(env.NOLO_LOCAL_SHELL_MODE || "");
53
71
  }
@@ -72,51 +90,62 @@ export function resolveLocalToolPolicy(args: {
72
90
  agentToolNames?: string[];
73
91
  toolName: string;
74
92
  }): LocalToolPolicyDecision {
93
+ const toolName = normalizeLocalToolName(args.toolName);
75
94
  const restrictedMode = isRestrictedLocalToolMode(args.env);
76
- if (args.toolName === "execShell") {
95
+ if (!toolName) {
96
+ return {
97
+ allowed: false,
98
+ toolName,
99
+ reason: "Tool name is required.",
100
+ };
101
+ }
102
+
103
+ if (toolName === "execShell") {
77
104
  if (isLocalShellModeEnabled(args.env)) {
78
- return { allowed: true, toolName: args.toolName };
105
+ return { allowed: true, toolName };
79
106
  }
80
107
  return {
81
108
  allowed: false,
82
- toolName: args.toolName,
109
+ toolName,
83
110
  reason: "execShell requires NOLO_LOCAL_SHELL_MODE=worktree.",
84
111
  };
85
112
  }
86
113
 
87
- if (NEVER_LOCAL_TOOLS.has(args.toolName)) {
114
+ if (NEVER_LOCAL_TOOLS.has(toolName)) {
88
115
  return {
89
116
  allowed: false,
90
- toolName: args.toolName,
91
- reason: `${args.toolName} is blocked by the local runtime safety policy.`,
117
+ toolName,
118
+ reason: `${toolName} is blocked by the local runtime safety policy.`,
92
119
  };
93
120
  }
94
121
 
95
- if (REMOVED_LOCAL_TOOLS.has(args.toolName)) {
122
+ if (REMOVED_LOCAL_TOOLS.has(toolName)) {
96
123
  return {
97
124
  allowed: false,
98
- toolName: args.toolName,
99
- reason: `${args.toolName} has been removed from the local runtime tool surface. Use execShell in shell-enabled workspace runs when shell is needed.`,
125
+ toolName,
126
+ reason: `${toolName} has been removed from the local runtime tool surface. Use execShell in shell-enabled workspace runs when shell is needed.`,
100
127
  };
101
128
  }
102
129
 
103
130
  const allowedByEnv = new Set(parseToolAllowlist(args.env.NOLO_LOCAL_ALLOWED_TOOLS));
104
- const agentTools = new Set(args.agentToolNames ?? []);
105
- if (!restrictedMode && agentTools.has(args.toolName)) {
106
- return { allowed: true, toolName: args.toolName };
131
+ const agentTools = new Set(
132
+ (args.agentToolNames ?? []).map(normalizeLocalToolName).filter(Boolean),
133
+ );
134
+ if (!restrictedMode && agentTools.has(toolName)) {
135
+ return { allowed: true, toolName };
107
136
  }
108
- if (DEFAULT_LOCAL_TOOLS.has(args.toolName) && agentTools.has(args.toolName)) {
109
- return { allowed: true, toolName: args.toolName };
137
+ if (DEFAULT_LOCAL_TOOLS.has(toolName) && agentTools.has(toolName)) {
138
+ return { allowed: true, toolName };
110
139
  }
111
- if (allowedByEnv.has(args.toolName) && agentTools.has(args.toolName)) {
112
- return { allowed: true, toolName: args.toolName };
140
+ if (allowedByEnv.has(toolName) && agentTools.has(toolName)) {
141
+ return { allowed: true, toolName };
113
142
  }
114
143
 
115
144
  return {
116
145
  allowed: false,
117
- toolName: args.toolName,
146
+ toolName,
118
147
  reason:
119
- `${args.toolName} is not enabled for local runtime runs. ` +
148
+ `${toolName} is not enabled for local runtime runs. ` +
120
149
  "Set NOLO_LOCAL_ALLOWED_TOOLS and make sure the agent declares the tool before using it locally.",
121
150
  };
122
151
  }
@@ -134,9 +163,29 @@ export async function executeLocalToolWithPolicy(args: {
134
163
  });
135
164
  if (!decision.allowed) throw new Error(decision.reason);
136
165
 
137
- const executor = args.executors?.[args.call.name];
166
+ const executor = args.executors?.[decision.toolName];
138
167
  if (!executor) {
139
- throw new Error(`${args.call.name} is allowed by policy but no local executor is registered.`);
168
+ throw new Error(`${decision.toolName} is allowed by policy but no local executor is registered.`);
140
169
  }
141
- return executor(args.call);
170
+ if (decision.toolName === "execShell") {
171
+ const parsed = parseShellCommandPayload(args.call.arguments);
172
+ const shellPolicy = evaluateShellCommandPolicy({
173
+ command: parsed.command ?? parsed.cmd,
174
+ input: parsed.input,
175
+ userInput: args.call.userInput,
176
+ });
177
+ if (shellPolicy.verdict === "forbidden") {
178
+ const error = new Error(shellPolicy.reason) as Error & {
179
+ code?: string;
180
+ policy?: Record<string, unknown>;
181
+ };
182
+ error.code = shellPolicy.code;
183
+ error.policy = shellPolicy.policy;
184
+ throw error;
185
+ }
186
+ }
187
+ return executor({
188
+ ...args.call,
189
+ name: decision.toolName,
190
+ });
142
191
  }
@@ -2,11 +2,13 @@ import type {
2
2
  AgentRuntimeChatMessage,
3
3
  AgentRuntimeResult,
4
4
  } from "./types";
5
+ import { buildProviderAuthHeaders } from "./providerResolution";
5
6
 
6
7
  export type OpenAiCompatibleProviderConfig = {
7
8
  model: string;
8
9
  endpoint: string;
9
10
  apiKey: string;
11
+ apiKeyHeader?: string;
10
12
  provider: string;
11
13
  requestOptions: Record<string, number | string>;
12
14
  };
@@ -41,9 +43,11 @@ export function buildOpenAiCompatibleChatCompletionRequest(args: {
41
43
  method: "POST",
42
44
  headers: {
43
45
  "Content-Type": "application/json",
44
- ...(args.providerConfig.apiKey
45
- ? { Authorization: `Bearer ${args.providerConfig.apiKey}` }
46
- : {}),
46
+ ...buildProviderAuthHeaders({
47
+ endpoint: args.providerConfig.endpoint,
48
+ apiKey: args.providerConfig.apiKey,
49
+ apiKeyHeader: args.providerConfig.apiKeyHeader,
50
+ }),
47
51
  },
48
52
  body: JSON.stringify(body),
49
53
  },
@@ -1,38 +1,27 @@
1
1
  import type { AgentRuntimeAgentConfig } from "./hostAdapter";
2
2
  import type { OpenAiCompatibleProviderConfig } from "./openAiCompatibleProvider";
3
- import { pickAgentRuntimeInferenceOptions } from "./agentConfigOptions";
3
+ import { buildProviderExecutionPlan } from "./providerResolution";
4
4
 
5
5
  type EnvLike = Record<string, string | undefined>;
6
6
 
7
- function trimTrailingSlash(value: string) {
8
- return value.replace(/\/+$/, "");
9
- }
10
-
11
- function resolveChatCompletionsEndpoint(value: string) {
12
- const trimmed = trimTrailingSlash(value.trim());
13
- return trimmed.endsWith("/chat/completions")
14
- ? trimmed
15
- : `${trimmed}/chat/completions`;
16
- }
17
-
18
- function resolveOpenAiCompatibleBaseUrl(env: EnvLike) {
19
- return env.NOLO_LOCAL_OPENAI_BASE_URL || env.OPENAI_BASE_URL || "https://api.openai.com/v1";
20
- }
21
-
22
- function resolveOpenAiCompatibleApiKey(env: EnvLike) {
23
- return env.OPENAI_API_KEY || env.NOLO_LOCAL_OPENAI_API_KEY || "";
24
- }
25
-
26
7
  export function resolveOpenAiCompatibleProviderConfig(args: {
27
8
  agentConfig: AgentRuntimeAgentConfig;
28
9
  env: EnvLike;
29
10
  }): OpenAiCompatibleProviderConfig {
30
- const customProviderUrl = args.agentConfig.customProviderUrl?.trim();
11
+ const plan = buildProviderExecutionPlan({
12
+ agentConfig: args.agentConfig,
13
+ env: args.env,
14
+ runtimeKind: "local",
15
+ });
16
+ if (plan.mode === "cli" || plan.transport !== "direct") {
17
+ throw new Error("OpenAI-compatible provider config requires a direct provider execution plan.");
18
+ }
31
19
  return {
32
- model: args.agentConfig.model || "gpt-4.1-mini",
33
- endpoint: resolveChatCompletionsEndpoint(customProviderUrl || resolveOpenAiCompatibleBaseUrl(args.env)),
34
- apiKey: resolveOpenAiCompatibleApiKey(args.env),
35
- provider: args.agentConfig.provider || "openai-compatible",
36
- requestOptions: pickAgentRuntimeInferenceOptions(args.agentConfig),
20
+ model: plan.model,
21
+ endpoint: plan.endpoint,
22
+ apiKey: plan.apiKey,
23
+ ...(plan.apiKeyHeader ? { apiKeyHeader: plan.apiKeyHeader } : {}),
24
+ provider: plan.provider,
25
+ requestOptions: plan.requestOptions,
37
26
  };
38
27
  }
@@ -3,23 +3,19 @@ import type {
3
3
  AgentRuntimeChatMessage,
4
4
  AgentRuntimeResult,
5
5
  } from "./types";
6
- import { pickAgentRuntimeInferenceOptions } from "./agentConfigOptions";
6
+ import {
7
+ buildProviderExecutionPlan,
8
+ canUsePlatformChatProvider as canUsePlatformChatProviderFromEnv,
9
+ hasDirectOpenAiCompatibleProvider as hasDirectOpenAiCompatibleProviderFromEnv,
10
+ resolvePlatformAuthToken,
11
+ resolvePlatformServerUrl,
12
+ shouldUsePlatformProxyTransport,
13
+ } from "./providerResolution";
7
14
 
8
15
  type EnvLike = Record<string, string | undefined>;
9
16
 
10
17
  const CHAT_PROXY_PATH = "/api/v1/chat";
11
18
 
12
- const PROVIDER_ENDPOINTS: Record<string, string> = {
13
- deepinfra: "https://api.deepinfra.com/v1/openai/chat/completions",
14
- deepseek: "https://api.deepseek.com/chat/completions",
15
- fireworks: "https://api.fireworks.ai/inference/v1/chat/completions",
16
- google: "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
17
- mistral: "https://api.mistral.ai/v1/chat/completions",
18
- mimo: "https://token-plan-cn.xiaomimimo.com/v1/chat/completions",
19
- openai: "https://api.openai.com/v1/chat/completions",
20
- openrouter: "https://openrouter.ai/api/v1/chat/completions",
21
- };
22
-
23
19
  export type PlatformChatProviderConfig = {
24
20
  serverUrl: string;
25
21
  authToken: string;
@@ -29,46 +25,12 @@ export type PlatformChatProviderConfig = {
29
25
  endpoint: string;
30
26
  requestOptions: Record<string, number | string>;
31
27
  apiKey?: string;
28
+ apiKeyHeader?: string;
32
29
  apiSource?: string;
33
30
  };
34
31
 
35
32
  type PlatformChatTool = Record<string, unknown>;
36
33
 
37
- function trimTrailingSlash(value: string) {
38
- return value.trim().replace(/\/+$/, "");
39
- }
40
-
41
- function resolveChatCompletionsEndpoint(value: string) {
42
- const trimmed = trimTrailingSlash(value);
43
- return trimmed.endsWith("/chat/completions")
44
- ? trimmed
45
- : `${trimmed}/chat/completions`;
46
- }
47
-
48
- function resolvePlatformServerUrl(env: EnvLike) {
49
- return trimTrailingSlash(env.NOLO_SERVER || env.BASE_URL || "https://nolo.chat");
50
- }
51
-
52
- function resolvePlatformAuthToken(env: EnvLike) {
53
- return env.AUTH_TOKEN || env.AUTH || env.BENCHMARK_AUTH_TOKEN || "";
54
- }
55
-
56
- function resolveProviderEndpoint(agentConfig: AgentRuntimeAgentConfig) {
57
- const customProviderUrl = agentConfig.customProviderUrl?.trim();
58
- if (customProviderUrl) return resolveChatCompletionsEndpoint(customProviderUrl);
59
-
60
- const provider = agentConfig.provider?.trim().toLowerCase();
61
- if (!provider) {
62
- throw new Error("Platform chat provider requires agentConfig.provider.");
63
- }
64
-
65
- const endpoint = PROVIDER_ENDPOINTS[provider];
66
- if (!endpoint) {
67
- throw new Error(`Platform chat provider does not support provider "${provider}".`);
68
- }
69
- return endpoint;
70
- }
71
-
72
34
  function shouldDisableThinking(providerConfig: PlatformChatProviderConfig) {
73
35
  return (
74
36
  providerConfig.provider.toLowerCase() === "mimo" ||
@@ -90,19 +52,39 @@ export function resolvePlatformChatProviderConfig(args: {
90
52
  agentConfig: AgentRuntimeAgentConfig;
91
53
  env: EnvLike;
92
54
  }): PlatformChatProviderConfig {
93
- const provider = args.agentConfig.provider || args.agentConfig.apiSource || "openai";
55
+ const plan = buildProviderExecutionPlan({
56
+ agentConfig: args.agentConfig,
57
+ env: args.env,
58
+ runtimeKind: "local",
59
+ });
60
+ if (plan.mode === "cli") {
61
+ throw new Error("Platform chat provider does not support cli agents.");
62
+ }
63
+ if (plan.transport === "direct") {
64
+ return {
65
+ serverUrl: resolvePlatformServerUrl(args.env),
66
+ authToken: resolvePlatformAuthToken(args.env),
67
+ agentKey: args.agentConfig.key,
68
+ model: plan.model,
69
+ provider: plan.provider,
70
+ endpoint: plan.endpoint,
71
+ requestOptions: plan.requestOptions,
72
+ ...(plan.apiKey ? { apiKey: plan.apiKey } : {}),
73
+ ...(plan.apiKeyHeader ? { apiKeyHeader: plan.apiKeyHeader } : {}),
74
+ ...(args.agentConfig.apiSource ? { apiSource: args.agentConfig.apiSource } : {}),
75
+ };
76
+ }
94
77
  return {
95
- serverUrl: resolvePlatformServerUrl(args.env),
96
- authToken: resolvePlatformAuthToken(args.env),
97
- agentKey: args.agentConfig.key,
98
- model: args.agentConfig.model || "gpt-4.1-mini",
99
- provider,
100
- endpoint: resolveProviderEndpoint(args.agentConfig),
101
- requestOptions: pickAgentRuntimeInferenceOptions(args.agentConfig),
102
- ...(typeof args.agentConfig.rawRecord?.apiKey === "string"
103
- ? { apiKey: args.agentConfig.rawRecord.apiKey.trim() }
104
- : {}),
105
- ...(args.agentConfig.apiSource ? { apiSource: args.agentConfig.apiSource } : {}),
78
+ serverUrl: plan.serverUrl,
79
+ authToken: plan.authToken,
80
+ agentKey: plan.agentKey,
81
+ model: plan.model,
82
+ provider: plan.provider,
83
+ endpoint: plan.endpoint,
84
+ requestOptions: plan.requestOptions,
85
+ ...(plan.apiKey ? { apiKey: plan.apiKey } : {}),
86
+ ...(plan.apiKeyHeader ? { apiKeyHeader: plan.apiKeyHeader } : {}),
87
+ ...(plan.apiSource ? { apiSource: plan.apiSource } : {}),
106
88
  };
107
89
  }
108
90
 
@@ -123,6 +105,7 @@ export function buildPlatformChatCompletionRequest(args: {
123
105
  agentKey: args.providerConfig.agentKey,
124
106
  ...(args.providerConfig.apiSource ? { apiSource: args.providerConfig.apiSource } : {}),
125
107
  ...(args.providerConfig.apiKey ? { KEY: args.providerConfig.apiKey } : {}),
108
+ ...(args.providerConfig.apiKeyHeader ? { apiKeyHeader: args.providerConfig.apiKeyHeader } : {}),
126
109
  };
127
110
 
128
111
  return {
@@ -216,21 +199,17 @@ export function parsePlatformChatCompletionResponse(args: {
216
199
  }
217
200
 
218
201
  export function canUsePlatformChatProvider(env: EnvLike) {
219
- return Boolean(resolvePlatformAuthToken(env));
202
+ return canUsePlatformChatProviderFromEnv(env);
220
203
  }
221
204
 
222
205
  export function hasDirectOpenAiCompatibleProvider(env: EnvLike) {
223
- return Boolean(
224
- env.NOLO_LOCAL_OPENAI_API_KEY ||
225
- env.OPENAI_API_KEY ||
226
- env.NOLO_LOCAL_OPENAI_BASE_URL ||
227
- env.OPENAI_BASE_URL ||
228
- env.OLLAMA_BASE_URL
229
- );
206
+ return hasDirectOpenAiCompatibleProviderFromEnv(env);
230
207
  }
231
208
 
232
209
  function agentRequiresPlatformChatProvider(agentConfig?: AgentRuntimeAgentConfig) {
233
- return agentConfig?.apiSource === "platform" || agentConfig?.rawRecord?.useServerProxy === true;
210
+ if (agentConfig?.apiSource === "platform") return true;
211
+ if (agentConfig?.apiSource === "custom" || agentConfig?.customProviderUrl?.trim()) return false;
212
+ return agentConfig?.useServerProxy === true;
234
213
  }
235
214
 
236
215
  export function shouldUsePlatformChatProvider(
@@ -238,7 +217,5 @@ export function shouldUsePlatformChatProvider(
238
217
  agentConfig?: AgentRuntimeAgentConfig
239
218
  ) {
240
219
  if (agentRequiresPlatformChatProvider(agentConfig)) return true;
241
- if (env.NOLO_LOCAL_LLM === "platform") return true;
242
- if (env.NOLO_LOCAL_LLM === "direct") return false;
243
- return !hasDirectOpenAiCompatibleProvider(env) && canUsePlatformChatProvider(env);
220
+ return shouldUsePlatformProxyTransport(env, agentConfig);
244
221
  }