@oneuptime/common 11.3.25 → 11.3.26
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/Models/DatabaseModels/AIConversation.ts +322 -0
- package/Models/DatabaseModels/AIConversationMessage.ts +495 -0
- package/Models/DatabaseModels/AIRun.ts +584 -0
- package/Models/DatabaseModels/AIRunEvent.ts +443 -0
- package/Models/DatabaseModels/Index.ts +8 -0
- package/Models/DatabaseModels/LlmLog.ts +26 -0
- package/Models/DatabaseModels/LlmProvider.ts +2 -2
- package/Server/API/AIChatAPI.ts +693 -0
- package/Server/API/LlmProviderAPI.ts +169 -0
- package/Server/API/MicrosoftTeamsAPI.ts +5 -0
- package/Server/API/SlackAPI.ts +663 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.ts +245 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.ts +36 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.ts +46 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.ts +119 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Services/AIConversationMessageService.ts +39 -0
- package/Server/Services/AIConversationService.ts +63 -0
- package/Server/Services/AIRunEventService.ts +39 -0
- package/Server/Services/AIRunService.ts +39 -0
- package/Server/Services/AIService.ts +53 -31
- package/Server/Services/DatabaseService.ts +19 -0
- package/Server/Services/LlmProviderService.ts +110 -0
- package/Server/Services/ProjectService.ts +24 -0
- package/Server/Types/AnalyticsDatabase/ModelPermission.ts +74 -9
- package/Server/Utils/AI/AIChatPrivacyFilter.ts +28 -0
- package/Server/Utils/AI/Chat/ChatAgentRunner.ts +1054 -0
- package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +239 -0
- package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +51 -0
- package/Server/Utils/AI/Toolbox/AlertTools.ts +201 -0
- package/Server/Utils/AI/Toolbox/AlertWriteTools.ts +174 -0
- package/Server/Utils/AI/Toolbox/ContextTools.ts +189 -0
- package/Server/Utils/AI/Toolbox/ExceptionTools.ts +149 -0
- package/Server/Utils/AI/Toolbox/IncidentTools.ts +204 -0
- package/Server/Utils/AI/Toolbox/IncidentWriteTools.ts +350 -0
- package/Server/Utils/AI/Toolbox/Index.ts +228 -0
- package/Server/Utils/AI/Toolbox/LogTools.ts +339 -0
- package/Server/Utils/AI/Toolbox/MetricTools.ts +179 -0
- package/Server/Utils/AI/Toolbox/MonitorTools.ts +193 -0
- package/Server/Utils/AI/Toolbox/Serializer.ts +257 -0
- package/Server/Utils/AI/Toolbox/ToolTypes.ts +249 -0
- package/Server/Utils/AI/Toolbox/TraceTools.ts +402 -0
- package/Server/Utils/AI/Toolbox/WidgetBuilder.ts +189 -0
- package/Server/Utils/LLM/LLMService.ts +502 -92
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +512 -19
- package/Server/Utils/Workspace/Slack/Slack.ts +80 -0
- package/Server/Utils/Workspace/Slack/app-manifest.json +16 -1
- package/Tests/Server/Utils/AI/AIChatModelACL.test.ts +42 -0
- package/Tests/Server/Utils/AI/ChatAgentHelpers.test.ts +89 -0
- package/Tests/Server/Utils/AI/LLMServiceBaseUrl.test.ts +139 -0
- package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +308 -0
- package/Tests/Server/Utils/AI/ToolArgsGetTimeRange.test.ts +62 -0
- package/Tests/Server/Utils/AI/ToolArgsScopeServiceIds.test.ts +79 -0
- package/Tests/Server/Utils/AI/ToolResultSerializer.test.ts +155 -0
- package/Tests/Types/Billing/SubscriptionStatus.test.ts +127 -0
- package/Tests/Types/Metrics/RecordingRuleDefinition.test.ts +213 -0
- package/Types/AI/AIChatMessageRole.ts +6 -0
- package/Types/AI/AIChatMessageStatus.ts +32 -0
- package/Types/AI/AIChatPermissionMode.ts +69 -0
- package/Types/AI/AIChatTypes.ts +231 -0
- package/Types/AI/AIRunEventType.ts +17 -0
- package/Types/AI/AIRunStatus.ts +28 -0
- package/Types/AI/AIRunType.ts +6 -0
- package/Types/LLM/LlmType.ts +6 -0
- package/UI/Components/Header/HeaderIconDropdownButton.tsx +22 -12
- package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +35 -3
- package/UI/Components/Page/Page.tsx +14 -0
- package/UI/Utils/LlmTypeDropdownOptions.ts +40 -0
- package/UI/Utils/TestLLMProvider.ts +59 -0
- package/build/dist/Models/DatabaseModels/AIConversation.js +345 -0
- package/build/dist/Models/DatabaseModels/AIConversation.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AIConversationMessage.js +521 -0
- package/build/dist/Models/DatabaseModels/AIConversationMessage.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AIRun.js +619 -0
- package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AIRunEvent.js +469 -0
- package/build/dist/Models/DatabaseModels/AIRunEvent.js.map +1 -0
- package/build/dist/Models/DatabaseModels/Index.js +8 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/LlmLog.js +28 -0
- package/build/dist/Models/DatabaseModels/LlmLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/LlmProvider.js +2 -2
- package/build/dist/Models/DatabaseModels/LlmProvider.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +498 -0
- package/build/dist/Server/API/AIChatAPI.js.map +1 -0
- package/build/dist/Server/API/LlmProviderAPI.js +107 -1
- package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +4 -0
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/SlackAPI.js +442 -0
- package/build/dist/Server/API/SlackAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.js +96 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.js +25 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.js +31 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.js +46 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AIConversationMessageService.js +34 -0
- package/build/dist/Server/Services/AIConversationMessageService.js.map +1 -0
- package/build/dist/Server/Services/AIConversationService.js +42 -0
- package/build/dist/Server/Services/AIConversationService.js.map +1 -0
- package/build/dist/Server/Services/AIRunEventService.js +34 -0
- package/build/dist/Server/Services/AIRunEventService.js.map +1 -0
- package/build/dist/Server/Services/AIRunService.js +34 -0
- package/build/dist/Server/Services/AIRunService.js.map +1 -0
- package/build/dist/Server/Services/AIService.js +31 -23
- package/build/dist/Server/Services/AIService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +10 -0
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/LlmProviderService.js +108 -0
- package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +25 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +50 -7
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
- package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js +18 -0
- package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js.map +1 -0
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +745 -0
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +165 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +44 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js +167 -0
- package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/AlertWriteTools.js +136 -0
- package/build/dist/Server/Utils/AI/Toolbox/AlertWriteTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/ContextTools.js +141 -0
- package/build/dist/Server/Utils/AI/Toolbox/ContextTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/ExceptionTools.js +117 -0
- package/build/dist/Server/Utils/AI/Toolbox/ExceptionTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +167 -0
- package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/IncidentWriteTools.js +280 -0
- package/build/dist/Server/Utils/AI/Toolbox/IncidentWriteTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js +150 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/LogTools.js +246 -0
- package/build/dist/Server/Utils/AI/Toolbox/LogTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +120 -0
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js +158 -0
- package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/Serializer.js +188 -0
- package/build/dist/Server/Utils/AI/Toolbox/Serializer.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/ToolTypes.js +142 -0
- package/build/dist/Server/Utils/AI/Toolbox/ToolTypes.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/TraceTools.js +309 -0
- package/build/dist/Server/Utils/AI/Toolbox/TraceTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/WidgetBuilder.js +120 -0
- package/build/dist/Server/Utils/AI/Toolbox/WidgetBuilder.js.map +1 -0
- package/build/dist/Server/Utils/LLM/LLMService.js +372 -88
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +378 -15
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js +59 -0
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/app-manifest.json +16 -1
- package/build/dist/Types/AI/AIChatMessageRole.js +7 -0
- package/build/dist/Types/AI/AIChatMessageRole.js.map +1 -0
- package/build/dist/Types/AI/AIChatMessageStatus.js +27 -0
- package/build/dist/Types/AI/AIChatMessageStatus.js.map +1 -0
- package/build/dist/Types/AI/AIChatPermissionMode.js +53 -0
- package/build/dist/Types/AI/AIChatPermissionMode.js.map +1 -0
- package/build/dist/Types/AI/AIChatTypes.js +74 -0
- package/build/dist/Types/AI/AIChatTypes.js.map +1 -0
- package/build/dist/Types/AI/AIRunEventType.js +18 -0
- package/build/dist/Types/AI/AIRunEventType.js.map +1 -0
- package/build/dist/Types/AI/AIRunStatus.js +26 -0
- package/build/dist/Types/AI/AIRunStatus.js.map +1 -0
- package/build/dist/Types/AI/AIRunType.js +7 -0
- package/build/dist/Types/AI/AIRunType.js.map +1 -0
- package/build/dist/Types/LLM/LlmType.js +6 -0
- package/build/dist/Types/LLM/LlmType.js.map +1 -1
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +10 -5
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +18 -4
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/Page/Page.js +3 -1
- package/build/dist/UI/Components/Page/Page.js.map +1 -1
- package/build/dist/UI/Utils/LlmTypeDropdownOptions.js +38 -0
- package/build/dist/UI/Utils/LlmTypeDropdownOptions.js.map +1 -0
- package/build/dist/UI/Utils/TestLLMProvider.js +37 -0
- package/build/dist/UI/Utils/TestLLMProvider.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,21 +1,47 @@
|
|
|
1
1
|
import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
|
|
2
2
|
import HTTPResponse from "../../../Types/API/HTTPResponse";
|
|
3
3
|
import URL from "../../../Types/API/URL";
|
|
4
|
-
import { JSONObject } from "../../../Types/JSON";
|
|
4
|
+
import { JSONArray, JSONObject } from "../../../Types/JSON";
|
|
5
|
+
import JSONFunctions from "../../../Types/JSONFunctions";
|
|
5
6
|
import API from "../../../Utils/API";
|
|
6
7
|
import LlmType from "../../../Types/LLM/LlmType";
|
|
7
8
|
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
8
9
|
import logger, { LogAttributes } from "../Logger";
|
|
9
10
|
import CaptureSpan from "../Telemetry/CaptureSpan";
|
|
10
11
|
|
|
12
|
+
export interface LLMToolDefinition {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
// JSON Schema for the tool's arguments.
|
|
16
|
+
inputSchema: JSONObject;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LLMToolCall {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
arguments: JSONObject;
|
|
23
|
+
/*
|
|
24
|
+
* Set when the provider returned malformed argument JSON that could not be
|
|
25
|
+
* parsed. Callers must NOT execute the tool with the empty arguments —
|
|
26
|
+
* surface the error to the model so it can retry.
|
|
27
|
+
*/
|
|
28
|
+
argumentsParseError?: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
11
31
|
export interface LLMMessage {
|
|
12
|
-
role: "system" | "user" | "assistant";
|
|
32
|
+
role: "system" | "user" | "assistant" | "tool";
|
|
13
33
|
content: string;
|
|
34
|
+
// Set on assistant messages that requested tool calls.
|
|
35
|
+
toolCalls?: Array<LLMToolCall> | undefined;
|
|
36
|
+
// Set on tool messages: which tool call this result answers.
|
|
37
|
+
toolCallId?: string | undefined;
|
|
14
38
|
}
|
|
15
39
|
|
|
16
40
|
export interface LLMCompletionRequest {
|
|
17
41
|
messages: Array<LLMMessage>;
|
|
18
|
-
temperature?: number;
|
|
42
|
+
temperature?: number | undefined;
|
|
43
|
+
maxTokens?: number | undefined;
|
|
44
|
+
tools?: Array<LLMToolDefinition> | undefined;
|
|
19
45
|
llmProviderConfig: LLMProviderConfig;
|
|
20
46
|
}
|
|
21
47
|
|
|
@@ -27,6 +53,8 @@ export interface LLMUsage {
|
|
|
27
53
|
|
|
28
54
|
export interface LLMCompletionResponse {
|
|
29
55
|
content: string;
|
|
56
|
+
toolCalls?: Array<LLMToolCall> | undefined;
|
|
57
|
+
stopReason?: "stop" | "tool_use" | undefined;
|
|
30
58
|
usage: LLMUsage | undefined;
|
|
31
59
|
}
|
|
32
60
|
|
|
@@ -48,6 +76,7 @@ export default class LLMService {
|
|
|
48
76
|
case LlmType.OpenAI:
|
|
49
77
|
case LlmType.Groq:
|
|
50
78
|
case LlmType.Mistral:
|
|
79
|
+
case LlmType.OpenAICompatible:
|
|
51
80
|
return await this.getOpenAICompatibleCompletion(config, request);
|
|
52
81
|
case LlmType.AzureOpenAI:
|
|
53
82
|
return await this.getAzureOpenAICompletion(config, request);
|
|
@@ -60,15 +89,277 @@ export default class LLMService {
|
|
|
60
89
|
}
|
|
61
90
|
}
|
|
62
91
|
|
|
92
|
+
/*
|
|
93
|
+
* OpenAI-compatible wire format (OpenAI, Groq, Mistral, Azure OpenAI).
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
private static toOpenAIMessages(
|
|
97
|
+
messages: Array<LLMMessage>,
|
|
98
|
+
): Array<JSONObject> {
|
|
99
|
+
return messages.map((msg: LLMMessage) => {
|
|
100
|
+
if (msg.role === "assistant" && msg.toolCalls && msg.toolCalls.length) {
|
|
101
|
+
return {
|
|
102
|
+
role: "assistant",
|
|
103
|
+
content: msg.content || null,
|
|
104
|
+
tool_calls: msg.toolCalls.map((toolCall: LLMToolCall) => {
|
|
105
|
+
return {
|
|
106
|
+
id: toolCall.id,
|
|
107
|
+
type: "function",
|
|
108
|
+
function: {
|
|
109
|
+
name: toolCall.name,
|
|
110
|
+
arguments: JSON.stringify(toolCall.arguments),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (msg.role === "tool") {
|
|
118
|
+
return {
|
|
119
|
+
role: "tool",
|
|
120
|
+
tool_call_id: msg.toolCallId || "",
|
|
121
|
+
content: msg.content,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
role: msg.role,
|
|
127
|
+
content: msg.content,
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private static toOpenAITools(
|
|
133
|
+
tools: Array<LLMToolDefinition>,
|
|
134
|
+
): Array<JSONObject> {
|
|
135
|
+
return tools.map((tool: LLMToolDefinition) => {
|
|
136
|
+
return {
|
|
137
|
+
type: "function",
|
|
138
|
+
function: {
|
|
139
|
+
name: tool.name,
|
|
140
|
+
description: tool.description,
|
|
141
|
+
parameters: tool.inputSchema,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private static parseOpenAIToolCalls(
|
|
148
|
+
message: JSONObject,
|
|
149
|
+
): Array<LLMToolCall> | undefined {
|
|
150
|
+
const rawToolCalls: JSONArray | undefined = message["tool_calls"] as
|
|
151
|
+
| JSONArray
|
|
152
|
+
| undefined;
|
|
153
|
+
|
|
154
|
+
if (!rawToolCalls || rawToolCalls.length === 0) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return rawToolCalls.map((rawToolCall: JSONObject, index: number) => {
|
|
159
|
+
const fn: JSONObject = (rawToolCall["function"] as JSONObject) || {};
|
|
160
|
+
const parsed: { arguments: JSONObject; error?: string | undefined } =
|
|
161
|
+
this.parseToolCallArguments((fn["arguments"] as string) || "{}");
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
id: (rawToolCall["id"] as string) || `tool_call_${index}`,
|
|
165
|
+
name: (fn["name"] as string) || "",
|
|
166
|
+
arguments: parsed.arguments,
|
|
167
|
+
argumentsParseError: parsed.error,
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/*
|
|
173
|
+
* Models sometimes emit slightly malformed argument JSON (trailing commas,
|
|
174
|
+
* single quotes). Try strict JSON first, then tolerant JSON5, and report a
|
|
175
|
+
* parse error instead of silently executing with empty arguments.
|
|
176
|
+
*/
|
|
177
|
+
private static parseToolCallArguments(rawArguments: string): {
|
|
178
|
+
arguments: JSONObject;
|
|
179
|
+
error?: string | undefined;
|
|
180
|
+
} {
|
|
181
|
+
try {
|
|
182
|
+
return { arguments: JSON.parse(rawArguments) };
|
|
183
|
+
} catch {
|
|
184
|
+
// fall through to tolerant parsing
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const parsed: JSONObject | unknown = JSONFunctions.parse(rawArguments);
|
|
189
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
190
|
+
return { arguments: parsed as JSONObject };
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
arguments: {},
|
|
194
|
+
error: "Tool arguments were not a JSON object.",
|
|
195
|
+
};
|
|
196
|
+
} catch {
|
|
197
|
+
return {
|
|
198
|
+
arguments: {},
|
|
199
|
+
error: "Tool arguments were malformed JSON and could not be parsed.",
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private static buildOpenAIRequestBody(
|
|
205
|
+
modelName: string,
|
|
206
|
+
request: LLMCompletionRequest,
|
|
207
|
+
): JSONObject {
|
|
208
|
+
const data: JSONObject = {
|
|
209
|
+
model: modelName,
|
|
210
|
+
messages: this.toOpenAIMessages(request.messages),
|
|
211
|
+
temperature: request.temperature ?? 0.7,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
if (request.maxTokens) {
|
|
215
|
+
data["max_tokens"] = request.maxTokens;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (request.tools && request.tools.length > 0) {
|
|
219
|
+
data["tools"] = this.toOpenAITools(request.tools);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return data;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private static parseOpenAIResponse(
|
|
226
|
+
jsonData: JSONObject,
|
|
227
|
+
providerName: string,
|
|
228
|
+
): LLMCompletionResponse {
|
|
229
|
+
const choices: Array<JSONObject> = jsonData["choices"] as Array<JSONObject>;
|
|
230
|
+
|
|
231
|
+
if (!choices || choices.length === 0) {
|
|
232
|
+
throw new BadDataException(`No response from ${providerName}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const message: JSONObject = choices[0]!["message"] as JSONObject;
|
|
236
|
+
const usage: JSONObject = jsonData["usage"] as JSONObject;
|
|
237
|
+
const toolCalls: Array<LLMToolCall> | undefined =
|
|
238
|
+
this.parseOpenAIToolCalls(message);
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
content: (message["content"] as string) || "",
|
|
242
|
+
toolCalls: toolCalls,
|
|
243
|
+
stopReason: toolCalls && toolCalls.length > 0 ? "tool_use" : "stop",
|
|
244
|
+
usage: usage
|
|
245
|
+
? {
|
|
246
|
+
promptTokens: usage["prompt_tokens"] as number,
|
|
247
|
+
completionTokens: usage["completion_tokens"] as number,
|
|
248
|
+
totalTokens: usage["total_tokens"] as number,
|
|
249
|
+
}
|
|
250
|
+
: undefined,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/*
|
|
255
|
+
* Build the chat completions URL for an OpenAI-compatible server from the
|
|
256
|
+
* configured base URL. Users enter the base URL in a few different ways, and
|
|
257
|
+
* we normalize all of them onto the right endpoint instead of 404-ing:
|
|
258
|
+
*
|
|
259
|
+
* - Already a full endpoint (".../chat/completions") -> used as-is.
|
|
260
|
+
* - Includes a path (".../v1", ".../openai/v1") -> append "/chat/completions".
|
|
261
|
+
* - Bare server root ("http://host:8000") -> append "/v1/chat/completions",
|
|
262
|
+
* since vLLM, LocalAI and similar servers expose the OpenAI-compatible API
|
|
263
|
+
* under /v1 by default. This is the most common self-hosted setup and the
|
|
264
|
+
* easiest one for users to get wrong (omitting /v1 returns FastAPI's
|
|
265
|
+
* {"detail":"Not Found"}).
|
|
266
|
+
*
|
|
267
|
+
* The endpoint segment is only ever appended to the PATH portion: any
|
|
268
|
+
* ?query or #fragment on the base URL is split off first and re-attached
|
|
269
|
+
* afterwards (otherwise "/chat/completions" would land inside the query or
|
|
270
|
+
* fragment). Trailing slashes are stripped so we never emit
|
|
271
|
+
* ".../v1//chat/completions", and the URL scheme is lower-cased because URL
|
|
272
|
+
* schemes are case-insensitive but downstream URL parsing only recognizes
|
|
273
|
+
* lowercase http/https.
|
|
274
|
+
*/
|
|
275
|
+
private static buildOpenAICompatibleChatCompletionsUrl(
|
|
276
|
+
baseUrl: string,
|
|
277
|
+
): string {
|
|
278
|
+
const raw: string = baseUrl.trim();
|
|
279
|
+
|
|
280
|
+
// Split off #fragment, then ?query, so we can operate on the path alone.
|
|
281
|
+
const fragmentIndex: number = raw.indexOf("#");
|
|
282
|
+
const fragment: string =
|
|
283
|
+
fragmentIndex >= 0 ? raw.substring(fragmentIndex) : "";
|
|
284
|
+
const withoutFragment: string =
|
|
285
|
+
fragmentIndex >= 0 ? raw.substring(0, fragmentIndex) : raw;
|
|
286
|
+
|
|
287
|
+
const queryIndex: number = withoutFragment.indexOf("?");
|
|
288
|
+
const query: string =
|
|
289
|
+
queryIndex >= 0 ? withoutFragment.substring(queryIndex) : "";
|
|
290
|
+
|
|
291
|
+
let base: string =
|
|
292
|
+
queryIndex >= 0
|
|
293
|
+
? withoutFragment.substring(0, queryIndex)
|
|
294
|
+
: withoutFragment;
|
|
295
|
+
|
|
296
|
+
/*
|
|
297
|
+
* Lower-case the scheme (e.g. "HTTP://" -> "http://") and strip trailing
|
|
298
|
+
* slashes.
|
|
299
|
+
*/
|
|
300
|
+
const schemeRegex: RegExp = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
301
|
+
base = base
|
|
302
|
+
.replace(schemeRegex, (scheme: string) => {
|
|
303
|
+
return scheme.toLowerCase();
|
|
304
|
+
})
|
|
305
|
+
.replace(/\/+$/, "");
|
|
306
|
+
|
|
307
|
+
let path: string;
|
|
308
|
+
const fullEndpointRegex: RegExp = /\/chat\/completions$/i;
|
|
309
|
+
const hasPathAfterHostRegex: RegExp = /^[a-z][a-z0-9+.-]*:\/\/[^/]+\/.+/i;
|
|
310
|
+
|
|
311
|
+
if (fullEndpointRegex.test(base)) {
|
|
312
|
+
// The user already pointed us at the full endpoint.
|
|
313
|
+
path = base;
|
|
314
|
+
} else if (hasPathAfterHostRegex.test(base)) {
|
|
315
|
+
/*
|
|
316
|
+
* The base URL carries a path after the host[:port] (e.g.
|
|
317
|
+
* "http://host:8000/v1" or ".../openai/v1") — trust it and only append
|
|
318
|
+
* the endpoint.
|
|
319
|
+
*/
|
|
320
|
+
path = `${base}/chat/completions`;
|
|
321
|
+
} else {
|
|
322
|
+
/*
|
|
323
|
+
* Bare server root ("http://host:8000") — add the conventional /v1
|
|
324
|
+
* prefix that OpenAI-compatible servers (vLLM, LocalAI, ...) use.
|
|
325
|
+
*/
|
|
326
|
+
path = `${base}/v1/chat/completions`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return `${path}${query}${fragment}`;
|
|
330
|
+
}
|
|
331
|
+
|
|
63
332
|
@CaptureSpan()
|
|
64
333
|
private static async getOpenAICompatibleCompletion(
|
|
65
334
|
config: LLMProviderConfig,
|
|
66
335
|
request: LLMCompletionRequest,
|
|
67
336
|
): Promise<LLMCompletionResponse> {
|
|
68
|
-
|
|
337
|
+
/*
|
|
338
|
+
* Generic OpenAI-compatible servers (vLLM, LocalAI, etc.) are usually
|
|
339
|
+
* self-hosted, frequently keyless, and have no canonical endpoint or
|
|
340
|
+
* default model — so the API key is optional but the base URL and model
|
|
341
|
+
* name must be provided. The hosted providers (OpenAI, Groq, Mistral)
|
|
342
|
+
* keep requiring a key and fall back to sensible defaults.
|
|
343
|
+
*/
|
|
344
|
+
const isGenericOpenAICompatible: boolean =
|
|
345
|
+
config.llmType === LlmType.OpenAICompatible;
|
|
346
|
+
|
|
347
|
+
if (!isGenericOpenAICompatible && !config.apiKey) {
|
|
69
348
|
throw new BadDataException(`${config.llmType} API key is required`);
|
|
70
349
|
}
|
|
71
350
|
|
|
351
|
+
if (isGenericOpenAICompatible && !config.baseUrl) {
|
|
352
|
+
throw new BadDataException(
|
|
353
|
+
"Base URL is required for OpenAI-compatible providers (e.g. http://your-vllm-server:8000/v1)",
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (isGenericOpenAICompatible && !config.modelName) {
|
|
358
|
+
throw new BadDataException(
|
|
359
|
+
"Model Name is required for OpenAI-compatible providers. It must match a model your server exposes.",
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
72
363
|
const defaultBaseUrls: Record<string, string> = {
|
|
73
364
|
[LlmType.OpenAI]: "https://api.openai.com/v1",
|
|
74
365
|
[LlmType.Groq]: "https://api.groq.com/openai/v1",
|
|
@@ -89,20 +380,20 @@ export default class LLMService {
|
|
|
89
380
|
config.modelName || defaultModels[config.llmType] || "gpt-4o";
|
|
90
381
|
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
|
91
382
|
await API.post<JSONObject>({
|
|
92
|
-
url: URL.fromString(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
role: msg.role,
|
|
98
|
-
content: msg.content,
|
|
99
|
-
};
|
|
100
|
-
}),
|
|
101
|
-
temperature: request.temperature ?? 0.7,
|
|
102
|
-
},
|
|
383
|
+
url: URL.fromString(
|
|
384
|
+
this.buildOpenAICompatibleChatCompletionsUrl(baseUrl),
|
|
385
|
+
),
|
|
386
|
+
data: this.buildOpenAIRequestBody(modelName, request),
|
|
103
387
|
headers: {
|
|
104
|
-
Authorization: `Bearer ${config.apiKey}`,
|
|
105
388
|
"Content-Type": "application/json",
|
|
389
|
+
/*
|
|
390
|
+
* Only send Authorization when a key is configured — a keyless
|
|
391
|
+
* server (e.g. vLLM started without --api-key) rejects an empty
|
|
392
|
+
* bearer token.
|
|
393
|
+
*/
|
|
394
|
+
...(config.apiKey
|
|
395
|
+
? { Authorization: `Bearer ${config.apiKey}` }
|
|
396
|
+
: {}),
|
|
106
397
|
},
|
|
107
398
|
options: {
|
|
108
399
|
retries: 2,
|
|
@@ -124,26 +415,10 @@ export default class LLMService {
|
|
|
124
415
|
);
|
|
125
416
|
}
|
|
126
417
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
throw new BadDataException(`No response from ${config.llmType}`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const message: JSONObject = choices[0]!["message"] as JSONObject;
|
|
135
|
-
const usage: JSONObject = jsonData["usage"] as JSONObject;
|
|
136
|
-
|
|
137
|
-
return {
|
|
138
|
-
content: message["content"] as string,
|
|
139
|
-
usage: usage
|
|
140
|
-
? {
|
|
141
|
-
promptTokens: usage["prompt_tokens"] as number,
|
|
142
|
-
completionTokens: usage["completion_tokens"] as number,
|
|
143
|
-
totalTokens: usage["total_tokens"] as number,
|
|
144
|
-
}
|
|
145
|
-
: undefined,
|
|
146
|
-
};
|
|
418
|
+
return this.parseOpenAIResponse(
|
|
419
|
+
response.jsonData as JSONObject,
|
|
420
|
+
config.llmType,
|
|
421
|
+
);
|
|
147
422
|
}
|
|
148
423
|
|
|
149
424
|
/*
|
|
@@ -192,16 +467,7 @@ export default class LLMService {
|
|
|
192
467
|
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
|
193
468
|
await API.post<JSONObject>({
|
|
194
469
|
url: URL.fromString(requestUrl),
|
|
195
|
-
data:
|
|
196
|
-
model: modelName,
|
|
197
|
-
messages: request.messages.map((msg: LLMMessage) => {
|
|
198
|
-
return {
|
|
199
|
-
role: msg.role,
|
|
200
|
-
content: msg.content,
|
|
201
|
-
};
|
|
202
|
-
}),
|
|
203
|
-
temperature: request.temperature ?? 0.7,
|
|
204
|
-
},
|
|
470
|
+
data: this.buildOpenAIRequestBody(modelName, request),
|
|
205
471
|
headers: {
|
|
206
472
|
"api-key": config.apiKey,
|
|
207
473
|
"Content-Type": "application/json",
|
|
@@ -226,26 +492,86 @@ export default class LLMService {
|
|
|
226
492
|
);
|
|
227
493
|
}
|
|
228
494
|
|
|
229
|
-
|
|
230
|
-
|
|
495
|
+
return this.parseOpenAIResponse(
|
|
496
|
+
response.jsonData as JSONObject,
|
|
497
|
+
"Azure OpenAI",
|
|
498
|
+
);
|
|
499
|
+
}
|
|
231
500
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
501
|
+
/*
|
|
502
|
+
* Anthropic wire format. System message is hoisted, tool results ride in
|
|
503
|
+
* user messages as tool_result blocks, and max_tokens is required by the
|
|
504
|
+
* API.
|
|
505
|
+
*/
|
|
235
506
|
|
|
236
|
-
|
|
237
|
-
const usage: JSONObject = jsonData["usage"] as JSONObject;
|
|
507
|
+
private static readonly ANTHROPIC_DEFAULT_MAX_TOKENS: number = 4096;
|
|
238
508
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
509
|
+
private static toAnthropicMessages(
|
|
510
|
+
messages: Array<LLMMessage>,
|
|
511
|
+
): Array<JSONObject> {
|
|
512
|
+
const anthropicMessages: Array<JSONObject> = [];
|
|
513
|
+
|
|
514
|
+
for (const msg of messages) {
|
|
515
|
+
if (msg.role === "system") {
|
|
516
|
+
continue; // hoisted separately
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (msg.role === "assistant" && msg.toolCalls && msg.toolCalls.length) {
|
|
520
|
+
const contentBlocks: Array<JSONObject> = [];
|
|
521
|
+
|
|
522
|
+
if (msg.content) {
|
|
523
|
+
contentBlocks.push({ type: "text", text: msg.content });
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
for (const toolCall of msg.toolCalls) {
|
|
527
|
+
contentBlocks.push({
|
|
528
|
+
type: "tool_use",
|
|
529
|
+
id: toolCall.id,
|
|
530
|
+
name: toolCall.name,
|
|
531
|
+
input: toolCall.arguments,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
anthropicMessages.push({ role: "assistant", content: contentBlocks });
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (msg.role === "tool") {
|
|
540
|
+
const toolResultBlock: JSONObject = {
|
|
541
|
+
type: "tool_result",
|
|
542
|
+
tool_use_id: msg.toolCallId || "",
|
|
543
|
+
content: msg.content,
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
/*
|
|
547
|
+
* Tool results must be user messages. Merge consecutive tool
|
|
548
|
+
* results into one user message so roles keep alternating.
|
|
549
|
+
*/
|
|
550
|
+
const lastMessage: JSONObject | undefined =
|
|
551
|
+
anthropicMessages[anthropicMessages.length - 1];
|
|
552
|
+
|
|
553
|
+
if (
|
|
554
|
+
lastMessage &&
|
|
555
|
+
lastMessage["role"] === "user" &&
|
|
556
|
+
Array.isArray(lastMessage["content"])
|
|
557
|
+
) {
|
|
558
|
+
(lastMessage["content"] as Array<JSONObject>).push(toolResultBlock);
|
|
559
|
+
} else {
|
|
560
|
+
anthropicMessages.push({
|
|
561
|
+
role: "user",
|
|
562
|
+
content: [toolResultBlock],
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
anthropicMessages.push({
|
|
569
|
+
role: msg.role,
|
|
570
|
+
content: msg.content,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return anthropicMessages;
|
|
249
575
|
}
|
|
250
576
|
|
|
251
577
|
@CaptureSpan()
|
|
@@ -260,31 +586,36 @@ export default class LLMService {
|
|
|
260
586
|
const baseUrl: string = config.baseUrl || "https://api.anthropic.com/v1";
|
|
261
587
|
const modelName: string = config.modelName || "claude-sonnet-4-20250514";
|
|
262
588
|
|
|
263
|
-
// Anthropic requires system message to be separate
|
|
264
589
|
let systemMessage: string = "";
|
|
265
|
-
const userMessages: Array<{ role: string; content: string }> = [];
|
|
266
590
|
|
|
267
591
|
for (const msg of request.messages) {
|
|
268
592
|
if (msg.role === "system") {
|
|
269
593
|
systemMessage = msg.content;
|
|
270
|
-
} else {
|
|
271
|
-
userMessages.push({
|
|
272
|
-
role: msg.role,
|
|
273
|
-
content: msg.content,
|
|
274
|
-
});
|
|
275
594
|
}
|
|
276
595
|
}
|
|
277
596
|
|
|
278
597
|
const requestData: JSONObject = {
|
|
279
598
|
model: modelName,
|
|
280
|
-
messages:
|
|
599
|
+
messages: this.toAnthropicMessages(request.messages),
|
|
281
600
|
temperature: request.temperature ?? 0.7,
|
|
601
|
+
// Anthropic requires max_tokens on every request.
|
|
602
|
+
max_tokens: request.maxTokens || LLMService.ANTHROPIC_DEFAULT_MAX_TOKENS,
|
|
282
603
|
};
|
|
283
604
|
|
|
284
605
|
if (systemMessage) {
|
|
285
606
|
requestData["system"] = systemMessage;
|
|
286
607
|
}
|
|
287
608
|
|
|
609
|
+
if (request.tools && request.tools.length > 0) {
|
|
610
|
+
requestData["tools"] = request.tools.map((tool: LLMToolDefinition) => {
|
|
611
|
+
return {
|
|
612
|
+
name: tool.name,
|
|
613
|
+
description: tool.description,
|
|
614
|
+
input_schema: tool.inputSchema,
|
|
615
|
+
};
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
288
619
|
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
|
289
620
|
await API.post<JSONObject>({
|
|
290
621
|
url: URL.fromString(`${baseUrl}/messages`),
|
|
@@ -321,20 +652,37 @@ export default class LLMService {
|
|
|
321
652
|
throw new BadDataException("No response from Anthropic");
|
|
322
653
|
}
|
|
323
654
|
|
|
324
|
-
const textContent:
|
|
325
|
-
(
|
|
326
|
-
return
|
|
327
|
-
}
|
|
328
|
-
|
|
655
|
+
const textContent: string = content
|
|
656
|
+
.filter((block: JSONObject) => {
|
|
657
|
+
return block["type"] === "text";
|
|
658
|
+
})
|
|
659
|
+
.map((block: JSONObject) => {
|
|
660
|
+
return block["text"] as string;
|
|
661
|
+
})
|
|
662
|
+
.join("");
|
|
663
|
+
|
|
664
|
+
const toolCalls: Array<LLMToolCall> = content
|
|
665
|
+
.filter((block: JSONObject) => {
|
|
666
|
+
return block["type"] === "tool_use";
|
|
667
|
+
})
|
|
668
|
+
.map((block: JSONObject) => {
|
|
669
|
+
return {
|
|
670
|
+
id: (block["id"] as string) || "",
|
|
671
|
+
name: (block["name"] as string) || "",
|
|
672
|
+
arguments: (block["input"] as JSONObject) || {},
|
|
673
|
+
};
|
|
674
|
+
});
|
|
329
675
|
|
|
330
|
-
if (!textContent) {
|
|
676
|
+
if (!textContent && toolCalls.length === 0) {
|
|
331
677
|
throw new BadDataException("No text content in Anthropic response");
|
|
332
678
|
}
|
|
333
679
|
|
|
334
680
|
const usage: JSONObject = jsonData["usage"] as JSONObject;
|
|
335
681
|
|
|
336
682
|
return {
|
|
337
|
-
content: textContent
|
|
683
|
+
content: textContent,
|
|
684
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
685
|
+
stopReason: jsonData["stop_reason"] === "tool_use" ? "tool_use" : "stop",
|
|
338
686
|
usage: usage
|
|
339
687
|
? {
|
|
340
688
|
promptTokens: usage["input_tokens"] as number,
|
|
@@ -347,6 +695,11 @@ export default class LLMService {
|
|
|
347
695
|
};
|
|
348
696
|
}
|
|
349
697
|
|
|
698
|
+
/*
|
|
699
|
+
* Ollama native /api/chat. Deliberately NOT routed through the
|
|
700
|
+
* OpenAI-compatible branch: Ollama deployments are keyless by design and
|
|
701
|
+
* the OpenAI branch requires an API key.
|
|
702
|
+
*/
|
|
350
703
|
@CaptureSpan()
|
|
351
704
|
private static async getOllamaCompletion(
|
|
352
705
|
config: LLMProviderConfig,
|
|
@@ -358,22 +711,44 @@ export default class LLMService {
|
|
|
358
711
|
|
|
359
712
|
const modelName: string = config.modelName || "llama2";
|
|
360
713
|
|
|
714
|
+
const requestData: JSONObject = {
|
|
715
|
+
model: modelName,
|
|
716
|
+
messages: request.messages.map((msg: LLMMessage) => {
|
|
717
|
+
if (msg.role === "assistant" && msg.toolCalls && msg.toolCalls.length) {
|
|
718
|
+
return {
|
|
719
|
+
role: "assistant",
|
|
720
|
+
content: msg.content || "",
|
|
721
|
+
tool_calls: msg.toolCalls.map((toolCall: LLMToolCall) => {
|
|
722
|
+
return {
|
|
723
|
+
function: {
|
|
724
|
+
name: toolCall.name,
|
|
725
|
+
arguments: toolCall.arguments,
|
|
726
|
+
},
|
|
727
|
+
};
|
|
728
|
+
}),
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return {
|
|
733
|
+
role: msg.role,
|
|
734
|
+
content: msg.content,
|
|
735
|
+
};
|
|
736
|
+
}),
|
|
737
|
+
stream: false,
|
|
738
|
+
options: {
|
|
739
|
+
temperature: request.temperature ?? 0.7,
|
|
740
|
+
...(request.maxTokens ? { num_predict: request.maxTokens } : {}),
|
|
741
|
+
},
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
if (request.tools && request.tools.length > 0) {
|
|
745
|
+
requestData["tools"] = this.toOpenAITools(request.tools);
|
|
746
|
+
}
|
|
747
|
+
|
|
361
748
|
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
|
362
749
|
await API.post<JSONObject>({
|
|
363
750
|
url: URL.fromString(`${config.baseUrl}/api/chat`),
|
|
364
|
-
data:
|
|
365
|
-
model: modelName,
|
|
366
|
-
messages: request.messages.map((msg: LLMMessage) => {
|
|
367
|
-
return {
|
|
368
|
-
role: msg.role,
|
|
369
|
-
content: msg.content,
|
|
370
|
-
};
|
|
371
|
-
}),
|
|
372
|
-
stream: false,
|
|
373
|
-
options: {
|
|
374
|
-
temperature: request.temperature ?? 0.7,
|
|
375
|
-
},
|
|
376
|
-
},
|
|
751
|
+
data: requestData,
|
|
377
752
|
headers: {
|
|
378
753
|
"Content-Type": "application/json",
|
|
379
754
|
},
|
|
@@ -404,8 +779,43 @@ export default class LLMService {
|
|
|
404
779
|
throw new BadDataException("No response from Ollama");
|
|
405
780
|
}
|
|
406
781
|
|
|
782
|
+
const rawToolCalls: JSONArray | undefined = message["tool_calls"] as
|
|
783
|
+
| JSONArray
|
|
784
|
+
| undefined;
|
|
785
|
+
|
|
786
|
+
let toolCalls: Array<LLMToolCall> | undefined = undefined;
|
|
787
|
+
|
|
788
|
+
if (rawToolCalls && rawToolCalls.length > 0) {
|
|
789
|
+
toolCalls = rawToolCalls.map((rawToolCall: JSONObject, index: number) => {
|
|
790
|
+
const fn: JSONObject = (rawToolCall["function"] as JSONObject) || {};
|
|
791
|
+
|
|
792
|
+
let parsedArguments: JSONObject = {};
|
|
793
|
+
let parseError: string | undefined = undefined;
|
|
794
|
+
const rawArguments: unknown = fn["arguments"];
|
|
795
|
+
|
|
796
|
+
if (typeof rawArguments === "string") {
|
|
797
|
+
const parsed: { arguments: JSONObject; error?: string | undefined } =
|
|
798
|
+
this.parseToolCallArguments(rawArguments);
|
|
799
|
+
parsedArguments = parsed.arguments;
|
|
800
|
+
parseError = parsed.error;
|
|
801
|
+
} else if (rawArguments && typeof rawArguments === "object") {
|
|
802
|
+
parsedArguments = rawArguments as JSONObject;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Ollama does not return tool-call ids; synthesize stable ones.
|
|
806
|
+
return {
|
|
807
|
+
id: `tool_call_${index}`,
|
|
808
|
+
name: (fn["name"] as string) || "",
|
|
809
|
+
arguments: parsedArguments,
|
|
810
|
+
argumentsParseError: parseError,
|
|
811
|
+
};
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
|
|
407
815
|
return {
|
|
408
|
-
content: message["content"] as string,
|
|
816
|
+
content: (message["content"] as string) || "",
|
|
817
|
+
toolCalls: toolCalls,
|
|
818
|
+
stopReason: toolCalls && toolCalls.length > 0 ? "tool_use" : "stop",
|
|
409
819
|
usage: undefined, // Ollama doesn't provide token usage in the same way
|
|
410
820
|
};
|
|
411
821
|
}
|