@makefinks/daemon 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -14
- package/package.json +4 -2
- package/src/ai/copilot-client.ts +775 -0
- package/src/ai/daemon-ai.ts +32 -234
- package/src/ai/model-config.ts +55 -14
- package/src/ai/providers/capabilities.ts +16 -0
- package/src/ai/providers/copilot-provider.ts +632 -0
- package/src/ai/providers/openrouter-provider.ts +217 -0
- package/src/ai/providers/registry.ts +14 -0
- package/src/ai/providers/types.ts +31 -0
- package/src/ai/system-prompt.ts +16 -0
- package/src/ai/tools/subagents.ts +1 -1
- package/src/ai/tools/tool-registry.ts +22 -1
- package/src/ai/tools/write-file.ts +51 -0
- package/src/app/components/AppOverlays.tsx +9 -1
- package/src/app/components/ConversationPane.tsx +8 -2
- package/src/components/ModelMenu.tsx +202 -140
- package/src/components/OnboardingOverlay.tsx +147 -1
- package/src/components/SettingsMenu.tsx +27 -1
- package/src/components/TokenUsageDisplay.tsx +5 -3
- package/src/components/tool-layouts/layouts/index.ts +1 -0
- package/src/components/tool-layouts/layouts/write-file.tsx +117 -0
- package/src/hooks/daemon-event-handlers.ts +61 -14
- package/src/hooks/keyboard-handlers.ts +109 -28
- package/src/hooks/use-app-callbacks.ts +141 -43
- package/src/hooks/use-app-context-builder.ts +5 -0
- package/src/hooks/use-app-controller.ts +31 -2
- package/src/hooks/use-app-copilot-models-loader.ts +45 -0
- package/src/hooks/use-app-display-state.ts +24 -2
- package/src/hooks/use-app-model.ts +103 -17
- package/src/hooks/use-app-preferences-bootstrap.ts +54 -10
- package/src/hooks/use-bootstrap-controller.ts +5 -0
- package/src/hooks/use-daemon-events.ts +8 -2
- package/src/hooks/use-daemon-keyboard.ts +19 -6
- package/src/hooks/use-daemon-runtime-controller.ts +4 -0
- package/src/hooks/use-menu-keyboard.ts +6 -1
- package/src/state/app-context.tsx +6 -0
- package/src/types/index.ts +24 -1
- package/src/utils/copilot-models.ts +77 -0
- package/src/utils/preferences.ts +3 -0
package/src/ai/daemon-ai.ts
CHANGED
|
@@ -4,97 +4,33 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
type ModelMessage,
|
|
10
|
-
ToolLoopAgent,
|
|
11
|
-
generateText,
|
|
12
|
-
stepCountIs,
|
|
13
|
-
experimental_transcribe as transcribe,
|
|
14
|
-
} from "ai";
|
|
7
|
+
import { type ModelMessage, experimental_transcribe as transcribe } from "ai";
|
|
15
8
|
import { getDaemonManager } from "../state/daemon-state";
|
|
16
|
-
import { getRuntimeContext } from "../state/runtime-context";
|
|
17
9
|
import type {
|
|
18
10
|
MemoryToastOperation,
|
|
19
11
|
MemoryToastPreview,
|
|
20
12
|
ReasoningEffort,
|
|
21
13
|
StreamCallbacks,
|
|
22
|
-
TokenUsage,
|
|
23
|
-
ToolApprovalRequest,
|
|
24
|
-
ToolApprovalResponse,
|
|
25
14
|
TranscriptionResult,
|
|
26
15
|
} from "../types";
|
|
27
|
-
import { debug
|
|
28
|
-
import { getOpenRouterReportedCost } from "../utils/openrouter-reported-cost";
|
|
29
|
-
import { getWorkspacePath } from "../utils/workspace-manager";
|
|
16
|
+
import { debug } from "../utils/debug-logger";
|
|
30
17
|
import { buildMemoryInjection, getMemoryManager, isMemoryAvailable } from "./memory";
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import { type InteractionMode, buildDaemonSystemPrompt } from "./system-prompt";
|
|
35
|
-
import { coordinateToolApprovals } from "./tool-approval-coordinator";
|
|
36
|
-
import { getCachedToolAvailability, getDaemonTools } from "./tools/index";
|
|
18
|
+
import { TRANSCRIPTION_MODEL } from "./model-config";
|
|
19
|
+
import { getProviderAdapter } from "./providers/registry";
|
|
20
|
+
import { type InteractionMode } from "./system-prompt";
|
|
37
21
|
import { setSubagentProgressEmitter } from "./tools/subagents";
|
|
38
|
-
import { createToolAvailabilitySnapshot, resolveToolAvailability } from "./tools/tool-registry";
|
|
39
22
|
|
|
40
|
-
// Re-export ModelMessage from AI SDK since it's commonly needed by consumers
|
|
41
23
|
export type { ModelMessage } from "ai";
|
|
42
24
|
|
|
43
|
-
// OpenRouter client for AI SDK (response generation)
|
|
44
|
-
const openrouter = createOpenRouter();
|
|
45
|
-
|
|
46
|
-
// OpenAI client for transcription (OpenRouter doesn't support transcription)
|
|
47
25
|
const openai = createOpenAI({});
|
|
48
26
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
function normalizeStreamError(error: unknown): Error {
|
|
53
|
-
if (error instanceof Error) return error;
|
|
54
|
-
if (error && typeof error === "object" && "message" in error) {
|
|
55
|
-
const message = (error as { message?: unknown }).message;
|
|
56
|
-
if (typeof message === "string") return new Error(message);
|
|
27
|
+
async function buildMemoryInjectionForPrompt(userMessage: string): Promise<string | undefined> {
|
|
28
|
+
if (!getDaemonManager().memoryEnabled || !isMemoryAvailable()) {
|
|
29
|
+
return undefined;
|
|
57
30
|
}
|
|
58
|
-
return new Error(String(error));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* The DAEMON agent instance.
|
|
63
|
-
* Handles the agent loop internally, allowing for multi-step tool usage.
|
|
64
|
-
* Created dynamically to use the current model selection and reasoning effort.
|
|
65
|
-
* @param interactionMode - "text" for terminal output, "voice" for speech-optimized
|
|
66
|
-
* @param reasoningEffort - Optional reasoning effort level for models that support it
|
|
67
|
-
*/
|
|
68
|
-
async function createDaemonAgent(
|
|
69
|
-
interactionMode: InteractionMode = "text",
|
|
70
|
-
reasoningEffort?: ReasoningEffort,
|
|
71
|
-
memoryInjection?: string
|
|
72
|
-
) {
|
|
73
|
-
const modelConfig = buildOpenRouterChatSettings(
|
|
74
|
-
reasoningEffort ? { reasoning: { effort: reasoningEffort } } : undefined
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
const { sessionId } = getRuntimeContext();
|
|
78
|
-
const tools = await getDaemonTools();
|
|
79
|
-
const toolAvailability =
|
|
80
|
-
getCachedToolAvailability() ?? (await resolveToolAvailability(getDaemonManager().toolToggles));
|
|
81
31
|
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
return new ToolLoopAgent({
|
|
85
|
-
model: openrouter.chat(getResponseModel(), modelConfig),
|
|
86
|
-
instructions: buildDaemonSystemPrompt({
|
|
87
|
-
mode: interactionMode,
|
|
88
|
-
toolAvailability: createToolAvailabilitySnapshot(toolAvailability),
|
|
89
|
-
workspacePath,
|
|
90
|
-
memoryInjection,
|
|
91
|
-
}),
|
|
92
|
-
tools,
|
|
93
|
-
stopWhen: stepCountIs(MAX_AGENT_STEPS),
|
|
94
|
-
prepareStep: async ({ messages }) => ({
|
|
95
|
-
messages: sanitizeMessagesForInput(messages),
|
|
96
|
-
}),
|
|
97
|
-
});
|
|
32
|
+
const injection = await buildMemoryInjection(userMessage);
|
|
33
|
+
return injection || undefined;
|
|
98
34
|
}
|
|
99
35
|
|
|
100
36
|
/**
|
|
@@ -118,9 +54,8 @@ export async function transcribeAudio(
|
|
|
118
54
|
text: result.text,
|
|
119
55
|
};
|
|
120
56
|
} catch (error) {
|
|
121
|
-
// Check if this was an abort
|
|
122
57
|
if (error instanceof Error && error.name === "AbortError") {
|
|
123
|
-
throw error;
|
|
58
|
+
throw error;
|
|
124
59
|
}
|
|
125
60
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
126
61
|
throw new Error(`Transcription failed: ${err.message}`);
|
|
@@ -128,15 +63,8 @@ export async function transcribeAudio(
|
|
|
128
63
|
}
|
|
129
64
|
|
|
130
65
|
/**
|
|
131
|
-
* Generate a streaming response from DAEMON
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* @param userMessage - The transcribed user message
|
|
135
|
-
* @param callbacks - Callbacks for streaming tokens, tool calls, and completion
|
|
136
|
-
* @param conversationHistory - Previous AI SDK messages for context
|
|
137
|
-
* @param interactionMode - "text" for terminal output, "voice" for speech-optimized
|
|
138
|
-
* @param abortSignal - Optional abort signal to cancel the request
|
|
139
|
-
* @param reasoningEffort - Optional reasoning effort level for models that support it
|
|
66
|
+
* Generate a streaming response from DAEMON.
|
|
67
|
+
* Delegates provider-specific execution to the active provider adapter.
|
|
140
68
|
*/
|
|
141
69
|
export async function generateResponse(
|
|
142
70
|
userMessage: string,
|
|
@@ -146,7 +74,6 @@ export async function generateResponse(
|
|
|
146
74
|
abortSignal?: AbortSignal,
|
|
147
75
|
reasoningEffort?: ReasoningEffort
|
|
148
76
|
): Promise<void> {
|
|
149
|
-
// Set up subagent progress emitter to forward events to callbacks
|
|
150
77
|
setSubagentProgressEmitter({
|
|
151
78
|
onSubagentToolCall: (toolCallId: string, toolName: string, input?: unknown) => {
|
|
152
79
|
callbacks.onSubagentToolCall?.(toolCallId, toolName, input);
|
|
@@ -163,146 +90,30 @@ export async function generateResponse(
|
|
|
163
90
|
});
|
|
164
91
|
|
|
165
92
|
try {
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Add the user message
|
|
179
|
-
messages.push({ role: "user" as const, content: userMessage });
|
|
180
|
-
|
|
181
|
-
// Stream response from the agent with mode-specific system prompt
|
|
182
|
-
const agent = await createDaemonAgent(interactionMode, reasoningEffort, memoryInjection);
|
|
183
|
-
|
|
184
|
-
let currentMessages = messages;
|
|
185
|
-
let fullText = "";
|
|
186
|
-
let streamError: Error | null = null;
|
|
187
|
-
let allResponseMessages: ModelMessage[] = [];
|
|
188
|
-
|
|
189
|
-
while (true) {
|
|
190
|
-
const stream = await agent.stream({
|
|
191
|
-
messages: currentMessages,
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
const pendingApprovals: ToolApprovalRequest[] = [];
|
|
195
|
-
|
|
196
|
-
for await (const part of stream.fullStream) {
|
|
197
|
-
if (abortSignal?.aborted) {
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (part.type === "error") {
|
|
202
|
-
const err = normalizeStreamError(part.error);
|
|
203
|
-
streamError = err;
|
|
204
|
-
debug.error("agent-stream-error", {
|
|
205
|
-
message: err.message,
|
|
206
|
-
error: part.error,
|
|
207
|
-
});
|
|
208
|
-
callbacks.onError?.(err);
|
|
209
|
-
} else if (part.type === "abort") {
|
|
210
|
-
return;
|
|
211
|
-
} else if (part.type === "reasoning-delta") {
|
|
212
|
-
callbacks.onReasoningToken?.(part.text);
|
|
213
|
-
} else if (part.type === "text-delta") {
|
|
214
|
-
fullText += part.text;
|
|
215
|
-
callbacks.onToken?.(part.text);
|
|
216
|
-
} else if (part.type === "tool-input-start") {
|
|
217
|
-
callbacks.onToolCallStart?.(part.toolName, part.id);
|
|
218
|
-
} else if (part.type === "tool-call") {
|
|
219
|
-
callbacks.onToolCall?.(part.toolName, part.input, part.toolCallId);
|
|
220
|
-
} else if (part.type === "tool-result") {
|
|
221
|
-
callbacks.onToolResult?.(part.toolName, part.output, part.toolCallId);
|
|
222
|
-
} else if (part.type === "tool-error") {
|
|
223
|
-
const errorMessage = part.error instanceof Error ? part.error.message : String(part.error);
|
|
224
|
-
toolDebug.error("tool-error", {
|
|
225
|
-
toolName: part.toolName,
|
|
226
|
-
toolCallId: part.toolCallId,
|
|
227
|
-
input: part.input,
|
|
228
|
-
error: errorMessage,
|
|
229
|
-
});
|
|
230
|
-
callbacks.onToolResult?.(
|
|
231
|
-
part.toolName,
|
|
232
|
-
{ error: errorMessage, input: part.input },
|
|
233
|
-
part.toolCallId
|
|
234
|
-
);
|
|
235
|
-
} else if (part.type === "tool-approval-request") {
|
|
236
|
-
const approvalRequest: ToolApprovalRequest = {
|
|
237
|
-
approvalId: part.approvalId,
|
|
238
|
-
toolName: part.toolCall.toolName,
|
|
239
|
-
toolCallId: part.toolCall.toolCallId,
|
|
240
|
-
input: part.toolCall.input,
|
|
241
|
-
};
|
|
242
|
-
pendingApprovals.push(approvalRequest);
|
|
243
|
-
callbacks.onToolApprovalRequest?.(approvalRequest);
|
|
244
|
-
} else if (part.type === "finish-step") {
|
|
245
|
-
if (part.usage && callbacks.onStepUsage) {
|
|
246
|
-
const reportedCost = getOpenRouterReportedCost(part.providerMetadata);
|
|
247
|
-
|
|
248
|
-
// reportedCost may be undefined when provider doesn't supply it
|
|
249
|
-
|
|
250
|
-
callbacks.onStepUsage({
|
|
251
|
-
promptTokens: part.usage.inputTokens ?? 0,
|
|
252
|
-
completionTokens: part.usage.outputTokens ?? 0,
|
|
253
|
-
totalTokens: part.usage.totalTokens ?? 0,
|
|
254
|
-
reasoningTokens: part.usage.outputTokenDetails?.reasoningTokens ?? 0,
|
|
255
|
-
cachedInputTokens: part.usage.inputTokenDetails?.cacheReadTokens ?? 0,
|
|
256
|
-
cost: reportedCost,
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (streamError) {
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const rawResponseMessages = await stream.response.then((r) => r.messages);
|
|
267
|
-
const responseMessages = sanitizeMessagesForInput(rawResponseMessages);
|
|
268
|
-
allResponseMessages = [...allResponseMessages, ...responseMessages];
|
|
269
|
-
currentMessages = [...currentMessages, ...responseMessages];
|
|
270
|
-
|
|
271
|
-
if (pendingApprovals.length > 0 && callbacks.onAwaitingApprovals) {
|
|
272
|
-
const { toolMessage } = await coordinateToolApprovals({
|
|
273
|
-
pendingApprovals,
|
|
274
|
-
requestApprovals: callbacks.onAwaitingApprovals,
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
if (toolMessage) {
|
|
278
|
-
currentMessages = [...currentMessages, toolMessage];
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
break;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (streamError) {
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const finalText = extractFinalAssistantText(allResponseMessages);
|
|
93
|
+
const memoryInjection = await buildMemoryInjectionForPrompt(userMessage);
|
|
94
|
+
const provider = getProviderAdapter();
|
|
95
|
+
const result = await provider.streamResponse({
|
|
96
|
+
userMessage,
|
|
97
|
+
callbacks,
|
|
98
|
+
conversationHistory,
|
|
99
|
+
interactionMode,
|
|
100
|
+
abortSignal,
|
|
101
|
+
reasoningEffort,
|
|
102
|
+
memoryInjection,
|
|
103
|
+
});
|
|
292
104
|
|
|
293
|
-
if (!
|
|
294
|
-
callbacks.onError?.(new Error("Model returned empty response. Check API key and model availability."));
|
|
105
|
+
if (!result) {
|
|
295
106
|
return;
|
|
296
107
|
}
|
|
297
108
|
|
|
298
|
-
callbacks.onComplete?.(fullText,
|
|
109
|
+
callbacks.onComplete?.(result.fullText, result.responseMessages, result.usage, result.finalText);
|
|
299
110
|
|
|
300
|
-
|
|
111
|
+
const assistantTextForMemory = result.finalText ?? result.fullText;
|
|
112
|
+
void persistConversationMemory(userMessage, assistantTextForMemory).then((preview) => {
|
|
301
113
|
if (!preview) return;
|
|
302
114
|
callbacks.onMemorySaved?.(preview);
|
|
303
115
|
});
|
|
304
116
|
} catch (error) {
|
|
305
|
-
// Check if this was an abort - don't treat as error
|
|
306
117
|
if (abortSignal?.aborted) {
|
|
307
118
|
return;
|
|
308
119
|
}
|
|
@@ -310,10 +121,8 @@ export async function generateResponse(
|
|
|
310
121
|
return;
|
|
311
122
|
}
|
|
312
123
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
313
|
-
|
|
314
|
-
callbacks.onError?.(new Error(errorMessage));
|
|
124
|
+
callbacks.onError?.(new Error(err.message));
|
|
315
125
|
} finally {
|
|
316
|
-
// Clean up the subagent progress emitter
|
|
317
126
|
setSubagentProgressEmitter(null);
|
|
318
127
|
}
|
|
319
128
|
}
|
|
@@ -386,23 +195,12 @@ function truncatePreview(text: string, maxChars: number): string {
|
|
|
386
195
|
|
|
387
196
|
/**
|
|
388
197
|
* Generate a short descriptive title for a session based on the first user message.
|
|
389
|
-
* Uses the currently selected model.
|
|
390
|
-
* @param firstMessage - The first user message in the session
|
|
391
|
-
* @returns A short title (3-6 words) describing the session topic
|
|
392
198
|
*/
|
|
393
199
|
export async function generateSessionTitle(firstMessage: string): Promise<string> {
|
|
394
200
|
try {
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
messages: [
|
|
399
|
-
{
|
|
400
|
-
role: "user",
|
|
401
|
-
content: `Generate a short descriptive title for the following message <message>${firstMessage}</message>`,
|
|
402
|
-
},
|
|
403
|
-
],
|
|
404
|
-
});
|
|
405
|
-
return result.text.trim() || "New Session";
|
|
201
|
+
const provider = getProviderAdapter();
|
|
202
|
+
const title = await provider.generateSessionTitle(firstMessage);
|
|
203
|
+
return title.trim() || "New Session";
|
|
406
204
|
} catch (error) {
|
|
407
205
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
408
206
|
debug.error("session-title-generation-failed", { message: err.message });
|
package/src/ai/model-config.ts
CHANGED
|
@@ -3,35 +3,66 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { OpenRouterChatSettings } from "@openrouter/ai-sdk-provider";
|
|
6
|
-
import type { ModelOption } from "../types";
|
|
6
|
+
import type { LlmProvider, ModelOption } from "../types";
|
|
7
7
|
import { loadManualConfig } from "../utils/config";
|
|
8
8
|
|
|
9
9
|
// Available models for selection (OpenRouter format)
|
|
10
|
-
export const
|
|
10
|
+
export const AVAILABLE_OPENROUTER_MODELS: ModelOption[] = [
|
|
11
11
|
{ id: "x-ai/grok-4.1-fast", name: "Grok 4.1 Fast" },
|
|
12
|
+
{ id: "arcee-ai/trinity-large-preview:free", name: "Trinity Large Preview" },
|
|
12
13
|
{ id: "z-ai/glm-4.7", name: "GLM 4.7" },
|
|
13
14
|
{ id: "minimax/minimax-m2.1", name: "Minimax M2.1" },
|
|
14
15
|
{ id: "google/gemini-3-flash-preview", name: "Gemini 3 Flash" },
|
|
15
16
|
{ id: "google/gemini-3-pro-preview", name: "Gemini 3 Pro" },
|
|
16
17
|
{ id: "openai/gpt-5.2", name: "GPT 5.2" },
|
|
17
|
-
{ id: "moonshotai/kimi-k2
|
|
18
|
+
{ id: "moonshotai/kimi-k2.5", name: "Kimi K2.5" },
|
|
18
19
|
{ id: "openai/gpt-oss-120b:exacto", name: "GPT-OSS-120" },
|
|
19
|
-
{ id: "mistralai/devstral-2512:free", name: "Mistral Devstral" },
|
|
20
20
|
{ id: "nvidia/nemotron-3-nano-30b-a3b:free", name: "Nemotron 3 Nano" },
|
|
21
21
|
];
|
|
22
22
|
|
|
23
|
-
// Default model
|
|
24
|
-
export const
|
|
23
|
+
// Default model IDs
|
|
24
|
+
export const DEFAULT_OPENROUTER_MODEL_ID = "z-ai/glm-4.7";
|
|
25
|
+
export const DEFAULT_COPILOT_MODEL_ID = "claude-sonnet-4.5";
|
|
26
|
+
export const DEFAULT_MODEL_ID = DEFAULT_OPENROUTER_MODEL_ID;
|
|
27
|
+
export const DEFAULT_MODEL_PROVIDER: LlmProvider = "openrouter";
|
|
25
28
|
|
|
26
|
-
//
|
|
27
|
-
|
|
29
|
+
// Backward-compatible alias used by existing OpenRouter pricing loaders.
|
|
30
|
+
export const AVAILABLE_MODELS = AVAILABLE_OPENROUTER_MODELS;
|
|
31
|
+
|
|
32
|
+
// Current selected provider + model IDs (mutable)
|
|
33
|
+
let currentModelProvider: LlmProvider = DEFAULT_MODEL_PROVIDER;
|
|
34
|
+
const currentModelIdByProvider: Record<LlmProvider, string> = {
|
|
35
|
+
openrouter: DEFAULT_OPENROUTER_MODEL_ID,
|
|
36
|
+
copilot: DEFAULT_COPILOT_MODEL_ID,
|
|
37
|
+
};
|
|
28
38
|
let currentOpenRouterProviderTag: string | undefined;
|
|
29
39
|
|
|
30
40
|
/**
|
|
31
41
|
* Get the current response model ID.
|
|
32
42
|
*/
|
|
33
43
|
export function getResponseModel(): string {
|
|
34
|
-
return
|
|
44
|
+
return currentModelIdByProvider[currentModelProvider];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get selected model ID for a specific provider.
|
|
49
|
+
*/
|
|
50
|
+
export function getResponseModelForProvider(provider: LlmProvider): string {
|
|
51
|
+
return currentModelIdByProvider[provider];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get the currently selected LLM provider.
|
|
56
|
+
*/
|
|
57
|
+
export function getModelProvider(): LlmProvider {
|
|
58
|
+
return currentModelProvider;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Set the currently selected LLM provider.
|
|
63
|
+
*/
|
|
64
|
+
export function setModelProvider(provider: LlmProvider): void {
|
|
65
|
+
currentModelProvider = provider;
|
|
35
66
|
}
|
|
36
67
|
|
|
37
68
|
/**
|
|
@@ -57,10 +88,20 @@ export function setOpenRouterProviderTag(providerTag: string | undefined): void
|
|
|
57
88
|
*/
|
|
58
89
|
export function setResponseModel(modelId: string): void {
|
|
59
90
|
if (!modelId) return;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
91
|
+
setResponseModelForProvider(currentModelProvider, modelId);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Set model ID for a specific provider.
|
|
96
|
+
*/
|
|
97
|
+
export function setResponseModelForProvider(provider: LlmProvider, modelId: string): void {
|
|
98
|
+
if (!modelId) return;
|
|
99
|
+
if (modelId !== currentModelIdByProvider[provider]) {
|
|
100
|
+
currentModelIdByProvider[provider] = modelId;
|
|
101
|
+
// Reset OpenRouter routing provider when switching OpenRouter models.
|
|
102
|
+
if (provider === "openrouter") {
|
|
103
|
+
currentOpenRouterProviderTag = undefined;
|
|
104
|
+
}
|
|
64
105
|
}
|
|
65
106
|
}
|
|
66
107
|
|
|
@@ -68,7 +109,7 @@ export function setResponseModel(modelId: string): void {
|
|
|
68
109
|
* Get the current subagent model ID (same as main agent).
|
|
69
110
|
*/
|
|
70
111
|
export function getSubagentModel(): string {
|
|
71
|
-
return
|
|
112
|
+
return getResponseModel();
|
|
72
113
|
}
|
|
73
114
|
|
|
74
115
|
/**
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { LlmProvider } from "../../types";
|
|
2
|
+
import { getModelProvider } from "../model-config";
|
|
3
|
+
import type { ProviderCapabilities } from "./types";
|
|
4
|
+
|
|
5
|
+
const PROVIDER_CAPABILITIES: Record<LlmProvider, ProviderCapabilities> = {
|
|
6
|
+
openrouter: {
|
|
7
|
+
supportsSubagentTool: true,
|
|
8
|
+
},
|
|
9
|
+
copilot: {
|
|
10
|
+
supportsSubagentTool: false,
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function getProviderCapabilities(provider: LlmProvider = getModelProvider()): ProviderCapabilities {
|
|
15
|
+
return PROVIDER_CAPABILITIES[provider];
|
|
16
|
+
}
|