@ottocode/server 0.1.173
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 +42 -0
- package/src/events/bus.ts +43 -0
- package/src/events/types.ts +32 -0
- package/src/index.ts +281 -0
- package/src/openapi/helpers.ts +64 -0
- package/src/openapi/paths/ask.ts +70 -0
- package/src/openapi/paths/config.ts +218 -0
- package/src/openapi/paths/files.ts +72 -0
- package/src/openapi/paths/git.ts +457 -0
- package/src/openapi/paths/messages.ts +92 -0
- package/src/openapi/paths/sessions.ts +90 -0
- package/src/openapi/paths/setu.ts +154 -0
- package/src/openapi/paths/stream.ts +26 -0
- package/src/openapi/paths/terminals.ts +226 -0
- package/src/openapi/schemas.ts +345 -0
- package/src/openapi/spec.ts +49 -0
- package/src/presets.ts +85 -0
- package/src/routes/ask.ts +113 -0
- package/src/routes/auth.ts +592 -0
- package/src/routes/branch.ts +106 -0
- package/src/routes/config/agents.ts +44 -0
- package/src/routes/config/cwd.ts +21 -0
- package/src/routes/config/defaults.ts +45 -0
- package/src/routes/config/index.ts +16 -0
- package/src/routes/config/main.ts +73 -0
- package/src/routes/config/models.ts +139 -0
- package/src/routes/config/providers.ts +46 -0
- package/src/routes/config/utils.ts +120 -0
- package/src/routes/files.ts +218 -0
- package/src/routes/git/branch.ts +75 -0
- package/src/routes/git/commit.ts +209 -0
- package/src/routes/git/diff.ts +137 -0
- package/src/routes/git/index.ts +18 -0
- package/src/routes/git/push.ts +160 -0
- package/src/routes/git/schemas.ts +48 -0
- package/src/routes/git/staging.ts +208 -0
- package/src/routes/git/status.ts +83 -0
- package/src/routes/git/types.ts +31 -0
- package/src/routes/git/utils.ts +249 -0
- package/src/routes/openapi.ts +6 -0
- package/src/routes/research.ts +392 -0
- package/src/routes/root.ts +5 -0
- package/src/routes/session-approval.ts +63 -0
- package/src/routes/session-files.ts +387 -0
- package/src/routes/session-messages.ts +170 -0
- package/src/routes/session-stream.ts +61 -0
- package/src/routes/sessions.ts +814 -0
- package/src/routes/setu.ts +346 -0
- package/src/routes/terminals.ts +227 -0
- package/src/runtime/agent/registry.ts +351 -0
- package/src/runtime/agent/runner-reasoning.ts +108 -0
- package/src/runtime/agent/runner-setup.ts +257 -0
- package/src/runtime/agent/runner.ts +375 -0
- package/src/runtime/agent-registry.ts +6 -0
- package/src/runtime/ask/service.ts +369 -0
- package/src/runtime/context/environment.ts +202 -0
- package/src/runtime/debug/index.ts +117 -0
- package/src/runtime/debug/state.ts +140 -0
- package/src/runtime/errors/api-error.ts +192 -0
- package/src/runtime/errors/handling.ts +199 -0
- package/src/runtime/message/compaction-auto.ts +154 -0
- package/src/runtime/message/compaction-context.ts +101 -0
- package/src/runtime/message/compaction-detect.ts +26 -0
- package/src/runtime/message/compaction-limits.ts +37 -0
- package/src/runtime/message/compaction-mark.ts +111 -0
- package/src/runtime/message/compaction-prune.ts +75 -0
- package/src/runtime/message/compaction.ts +21 -0
- package/src/runtime/message/history-builder.ts +266 -0
- package/src/runtime/message/service.ts +468 -0
- package/src/runtime/message/tool-history-tracker.ts +204 -0
- package/src/runtime/prompt/builder.ts +167 -0
- package/src/runtime/provider/anthropic.ts +50 -0
- package/src/runtime/provider/copilot.ts +12 -0
- package/src/runtime/provider/google.ts +8 -0
- package/src/runtime/provider/index.ts +60 -0
- package/src/runtime/provider/moonshot.ts +8 -0
- package/src/runtime/provider/oauth-adapter.ts +237 -0
- package/src/runtime/provider/openai.ts +18 -0
- package/src/runtime/provider/opencode.ts +7 -0
- package/src/runtime/provider/openrouter.ts +7 -0
- package/src/runtime/provider/selection.ts +118 -0
- package/src/runtime/provider/setu.ts +126 -0
- package/src/runtime/provider/zai.ts +16 -0
- package/src/runtime/session/branch.ts +280 -0
- package/src/runtime/session/db-operations.ts +285 -0
- package/src/runtime/session/manager.ts +99 -0
- package/src/runtime/session/queue.ts +243 -0
- package/src/runtime/stream/abort-handler.ts +65 -0
- package/src/runtime/stream/error-handler.ts +371 -0
- package/src/runtime/stream/finish-handler.ts +101 -0
- package/src/runtime/stream/handlers.ts +5 -0
- package/src/runtime/stream/step-finish.ts +93 -0
- package/src/runtime/stream/types.ts +25 -0
- package/src/runtime/tools/approval.ts +180 -0
- package/src/runtime/tools/context.ts +83 -0
- package/src/runtime/tools/mapping.ts +154 -0
- package/src/runtime/tools/setup.ts +44 -0
- package/src/runtime/topup/manager.ts +110 -0
- package/src/runtime/utils/cwd.ts +69 -0
- package/src/runtime/utils/token.ts +35 -0
- package/src/tools/adapter.ts +634 -0
- package/src/tools/database/get-parent-session.ts +183 -0
- package/src/tools/database/get-session-context.ts +161 -0
- package/src/tools/database/index.ts +42 -0
- package/src/tools/database/present-session-links.ts +47 -0
- package/src/tools/database/query-messages.ts +160 -0
- package/src/tools/database/query-sessions.ts +126 -0
- package/src/tools/database/search-history.ts +135 -0
- package/src/types/sql-imports.d.ts +5 -0
- package/sst-env.d.ts +8 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSetuModel,
|
|
3
|
+
catalog,
|
|
4
|
+
type SetuPaymentCallbacks,
|
|
5
|
+
getAuth,
|
|
6
|
+
loadConfig,
|
|
7
|
+
} from '@ottocode/sdk';
|
|
8
|
+
import { publish } from '../../events/bus.ts';
|
|
9
|
+
import {
|
|
10
|
+
waitForTopupMethodSelection,
|
|
11
|
+
type TopupMethod,
|
|
12
|
+
} from '../topup/manager.ts';
|
|
13
|
+
|
|
14
|
+
const MIN_TOPUP_USD = 5;
|
|
15
|
+
|
|
16
|
+
function getProviderNpm(model: string): string | undefined {
|
|
17
|
+
const entry = catalog.setu?.models?.find((m) => m.id === model);
|
|
18
|
+
return entry?.provider?.npm;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ResolveSetuModelOptions {
|
|
22
|
+
messageId?: string;
|
|
23
|
+
topupApprovalMode?: 'auto' | 'approval';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function getSetuPrivateKey(): Promise<string> {
|
|
27
|
+
if (process.env.SETU_PRIVATE_KEY) {
|
|
28
|
+
return process.env.SETU_PRIVATE_KEY;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const cfg = await loadConfig(process.cwd());
|
|
32
|
+
const auth = await getAuth('setu', cfg.projectRoot);
|
|
33
|
+
if (auth?.type === 'wallet' && auth.secret) {
|
|
34
|
+
return auth.secret;
|
|
35
|
+
}
|
|
36
|
+
} catch {}
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function resolveSetuModel(
|
|
41
|
+
model: string,
|
|
42
|
+
sessionId?: string,
|
|
43
|
+
options: ResolveSetuModelOptions = {},
|
|
44
|
+
) {
|
|
45
|
+
const privateKey = await getSetuPrivateKey();
|
|
46
|
+
if (!privateKey) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
'Setu provider requires SETU_PRIVATE_KEY (base58 Solana secret).',
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
const baseURL = process.env.SETU_BASE_URL;
|
|
52
|
+
const rpcURL = process.env.SETU_SOLANA_RPC_URL;
|
|
53
|
+
const { messageId, topupApprovalMode = 'approval' } = options;
|
|
54
|
+
|
|
55
|
+
const callbacks: SetuPaymentCallbacks = sessionId
|
|
56
|
+
? {
|
|
57
|
+
onPaymentRequired: (amountUsd, currentBalance) => {
|
|
58
|
+
publish({
|
|
59
|
+
type: 'setu.payment.required',
|
|
60
|
+
sessionId,
|
|
61
|
+
payload: { amountUsd, currentBalance },
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
onPaymentSigning: () => {
|
|
65
|
+
publish({
|
|
66
|
+
type: 'setu.payment.signing',
|
|
67
|
+
sessionId,
|
|
68
|
+
payload: {},
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
onPaymentComplete: (data) => {
|
|
72
|
+
publish({
|
|
73
|
+
type: 'setu.payment.complete',
|
|
74
|
+
sessionId,
|
|
75
|
+
payload: data,
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
onPaymentError: (error) => {
|
|
79
|
+
publish({
|
|
80
|
+
type: 'setu.payment.error',
|
|
81
|
+
sessionId,
|
|
82
|
+
payload: { error },
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
onPaymentApproval: async (info): Promise<TopupMethod | 'cancel'> => {
|
|
86
|
+
const suggestedTopupUsd = Math.max(
|
|
87
|
+
MIN_TOPUP_USD,
|
|
88
|
+
Math.ceil(info.amountUsd * 2),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
publish({
|
|
92
|
+
type: 'setu.topup.required',
|
|
93
|
+
sessionId,
|
|
94
|
+
payload: {
|
|
95
|
+
messageId,
|
|
96
|
+
amountUsd: info.amountUsd,
|
|
97
|
+
currentBalance: info.currentBalance,
|
|
98
|
+
minTopupUsd: MIN_TOPUP_USD,
|
|
99
|
+
suggestedTopupUsd,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return waitForTopupMethodSelection(
|
|
104
|
+
sessionId,
|
|
105
|
+
messageId ?? '',
|
|
106
|
+
info.amountUsd,
|
|
107
|
+
info.currentBalance,
|
|
108
|
+
);
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
: {};
|
|
112
|
+
|
|
113
|
+
const providerNpm = getProviderNpm(model);
|
|
114
|
+
|
|
115
|
+
return createSetuModel(
|
|
116
|
+
model,
|
|
117
|
+
{ privateKey },
|
|
118
|
+
{
|
|
119
|
+
baseURL,
|
|
120
|
+
rpcURL,
|
|
121
|
+
callbacks,
|
|
122
|
+
providerNpm,
|
|
123
|
+
topupApprovalMode,
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { OttoConfig } from '@ottocode/sdk';
|
|
2
|
+
import { getAuth, createZaiModel, createZaiCodingModel } from '@ottocode/sdk';
|
|
3
|
+
|
|
4
|
+
export async function getZaiInstance(cfg: OttoConfig, model: string) {
|
|
5
|
+
const auth = await getAuth('zai', cfg.projectRoot);
|
|
6
|
+
const apiKey = auth?.type === 'api' ? auth.key : undefined;
|
|
7
|
+
return createZaiModel(model, { apiKey });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function getZaiCodingInstance(cfg: OttoConfig, model: string) {
|
|
11
|
+
const auth =
|
|
12
|
+
(await getAuth('zai', cfg.projectRoot)) ||
|
|
13
|
+
(await getAuth('zai-coding', cfg.projectRoot));
|
|
14
|
+
const apiKey = auth?.type === 'api' ? auth.key : undefined;
|
|
15
|
+
return createZaiCodingModel(model, { apiKey });
|
|
16
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { eq, asc } from 'drizzle-orm';
|
|
2
|
+
import type { DB } from '@ottocode/database';
|
|
3
|
+
import { sessions, messages, messageParts } from '@ottocode/database/schema';
|
|
4
|
+
import { publish } from '../../events/bus.ts';
|
|
5
|
+
import type { ProviderId } from '@ottocode/sdk';
|
|
6
|
+
|
|
7
|
+
type SessionRow = typeof sessions.$inferSelect;
|
|
8
|
+
|
|
9
|
+
export type CreateBranchInput = {
|
|
10
|
+
db: DB;
|
|
11
|
+
parentSessionId: string;
|
|
12
|
+
fromMessageId: string;
|
|
13
|
+
provider?: ProviderId;
|
|
14
|
+
model?: string;
|
|
15
|
+
agent?: string;
|
|
16
|
+
title?: string;
|
|
17
|
+
projectPath: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type BranchResult = {
|
|
21
|
+
session: SessionRow;
|
|
22
|
+
parentSessionId: string;
|
|
23
|
+
branchPointMessageId: string;
|
|
24
|
+
copiedMessages: number;
|
|
25
|
+
copiedParts: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export async function createBranch({
|
|
29
|
+
db,
|
|
30
|
+
parentSessionId,
|
|
31
|
+
fromMessageId,
|
|
32
|
+
provider,
|
|
33
|
+
model,
|
|
34
|
+
agent,
|
|
35
|
+
title,
|
|
36
|
+
projectPath,
|
|
37
|
+
}: CreateBranchInput): Promise<BranchResult> {
|
|
38
|
+
const parentRows = await db
|
|
39
|
+
.select()
|
|
40
|
+
.from(sessions)
|
|
41
|
+
.where(eq(sessions.id, parentSessionId));
|
|
42
|
+
|
|
43
|
+
if (!parentRows.length) {
|
|
44
|
+
throw new Error('Parent session not found');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const parent = parentRows[0];
|
|
48
|
+
|
|
49
|
+
if (parent.projectPath !== projectPath) {
|
|
50
|
+
throw new Error('Parent session not found in this project');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const branchPointRows = await db
|
|
54
|
+
.select()
|
|
55
|
+
.from(messages)
|
|
56
|
+
.where(eq(messages.id, fromMessageId));
|
|
57
|
+
|
|
58
|
+
if (!branchPointRows.length) {
|
|
59
|
+
throw new Error('Branch point message not found');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const branchPoint = branchPointRows[0];
|
|
63
|
+
|
|
64
|
+
if (branchPoint.sessionId !== parentSessionId) {
|
|
65
|
+
throw new Error('Branch point message does not belong to parent session');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const allMessages = await db
|
|
69
|
+
.select()
|
|
70
|
+
.from(messages)
|
|
71
|
+
.where(eq(messages.sessionId, parentSessionId))
|
|
72
|
+
.orderBy(asc(messages.createdAt));
|
|
73
|
+
|
|
74
|
+
const branchPointIndex = allMessages.findIndex((m) => m.id === fromMessageId);
|
|
75
|
+
if (branchPointIndex === -1) {
|
|
76
|
+
throw new Error('Branch point message not found in session');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const messagesToCopy = allMessages.slice(0, branchPointIndex + 1);
|
|
80
|
+
|
|
81
|
+
const newSessionId = crypto.randomUUID();
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
|
|
84
|
+
const newSession: typeof sessions.$inferInsert = {
|
|
85
|
+
id: newSessionId,
|
|
86
|
+
title: title || `Branch of ${parent.title || 'Untitled'}`,
|
|
87
|
+
agent: agent || parent.agent,
|
|
88
|
+
provider: provider || parent.provider,
|
|
89
|
+
model: model || parent.model,
|
|
90
|
+
projectPath: parent.projectPath,
|
|
91
|
+
createdAt: now,
|
|
92
|
+
lastActiveAt: now,
|
|
93
|
+
parentSessionId,
|
|
94
|
+
branchPointMessageId: fromMessageId,
|
|
95
|
+
sessionType: 'branch',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
await db.insert(sessions).values(newSession);
|
|
99
|
+
|
|
100
|
+
const messageIdMap = new Map<string, string>();
|
|
101
|
+
let copiedParts = 0;
|
|
102
|
+
|
|
103
|
+
for (const msg of messagesToCopy) {
|
|
104
|
+
const newMessageId = crypto.randomUUID();
|
|
105
|
+
messageIdMap.set(msg.id, newMessageId);
|
|
106
|
+
|
|
107
|
+
const newMessage: typeof messages.$inferInsert = {
|
|
108
|
+
id: newMessageId,
|
|
109
|
+
sessionId: newSessionId,
|
|
110
|
+
role: msg.role,
|
|
111
|
+
status: msg.status,
|
|
112
|
+
agent: msg.agent,
|
|
113
|
+
provider: msg.provider,
|
|
114
|
+
model: msg.model,
|
|
115
|
+
createdAt: msg.createdAt,
|
|
116
|
+
completedAt: msg.completedAt,
|
|
117
|
+
latencyMs: msg.latencyMs,
|
|
118
|
+
inputTokens: msg.inputTokens,
|
|
119
|
+
outputTokens: msg.outputTokens,
|
|
120
|
+
totalTokens: msg.totalTokens,
|
|
121
|
+
cachedInputTokens: msg.cachedInputTokens,
|
|
122
|
+
cacheCreationInputTokens: msg.cacheCreationInputTokens,
|
|
123
|
+
reasoningTokens: msg.reasoningTokens,
|
|
124
|
+
error: msg.error,
|
|
125
|
+
errorType: msg.errorType,
|
|
126
|
+
errorDetails: msg.errorDetails,
|
|
127
|
+
isAborted: msg.isAborted,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
await db.insert(messages).values(newMessage);
|
|
131
|
+
|
|
132
|
+
const parts = await db
|
|
133
|
+
.select()
|
|
134
|
+
.from(messageParts)
|
|
135
|
+
.where(eq(messageParts.messageId, msg.id))
|
|
136
|
+
.orderBy(asc(messageParts.index));
|
|
137
|
+
|
|
138
|
+
for (const part of parts) {
|
|
139
|
+
const newPart: typeof messageParts.$inferInsert = {
|
|
140
|
+
id: crypto.randomUUID(),
|
|
141
|
+
messageId: newMessageId,
|
|
142
|
+
index: part.index,
|
|
143
|
+
stepIndex: part.stepIndex,
|
|
144
|
+
type: part.type,
|
|
145
|
+
content: part.content,
|
|
146
|
+
agent: part.agent,
|
|
147
|
+
provider: part.provider,
|
|
148
|
+
model: part.model,
|
|
149
|
+
startedAt: part.startedAt,
|
|
150
|
+
completedAt: part.completedAt,
|
|
151
|
+
compactedAt: part.compactedAt,
|
|
152
|
+
toolName: part.toolName,
|
|
153
|
+
toolCallId: part.toolCallId,
|
|
154
|
+
toolDurationMs: part.toolDurationMs,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
await db.insert(messageParts).values(newPart);
|
|
158
|
+
copiedParts++;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const result: SessionRow = {
|
|
163
|
+
...newSession,
|
|
164
|
+
totalInputTokens: null,
|
|
165
|
+
totalOutputTokens: null,
|
|
166
|
+
totalCachedTokens: null,
|
|
167
|
+
totalCacheCreationTokens: null,
|
|
168
|
+
totalReasoningTokens: null,
|
|
169
|
+
totalToolTimeMs: null,
|
|
170
|
+
toolCountsJson: null,
|
|
171
|
+
currentContextTokens: null,
|
|
172
|
+
contextSummary: null,
|
|
173
|
+
lastCompactedAt: null,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
publish({
|
|
177
|
+
type: 'session.created',
|
|
178
|
+
sessionId: newSessionId,
|
|
179
|
+
payload: result,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
session: result,
|
|
184
|
+
parentSessionId,
|
|
185
|
+
branchPointMessageId: fromMessageId,
|
|
186
|
+
copiedMessages: messagesToCopy.length,
|
|
187
|
+
copiedParts,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export type ListBranchesResult = Array<{
|
|
192
|
+
session: SessionRow;
|
|
193
|
+
branchPointMessageId: string | null;
|
|
194
|
+
branchPointPreview: string | null;
|
|
195
|
+
createdAt: number;
|
|
196
|
+
}>;
|
|
197
|
+
|
|
198
|
+
export async function listBranches(
|
|
199
|
+
db: DB,
|
|
200
|
+
sessionId: string,
|
|
201
|
+
projectPath: string,
|
|
202
|
+
): Promise<ListBranchesResult> {
|
|
203
|
+
const branches = await db
|
|
204
|
+
.select()
|
|
205
|
+
.from(sessions)
|
|
206
|
+
.where(eq(sessions.parentSessionId, sessionId))
|
|
207
|
+
.orderBy(asc(sessions.createdAt));
|
|
208
|
+
|
|
209
|
+
const results: ListBranchesResult = [];
|
|
210
|
+
|
|
211
|
+
for (const branch of branches) {
|
|
212
|
+
if (branch.projectPath !== projectPath) continue;
|
|
213
|
+
|
|
214
|
+
let preview: string | null = null;
|
|
215
|
+
|
|
216
|
+
if (branch.branchPointMessageId) {
|
|
217
|
+
const msgRows = await db
|
|
218
|
+
.select()
|
|
219
|
+
.from(messages)
|
|
220
|
+
.where(eq(messages.id, branch.branchPointMessageId));
|
|
221
|
+
|
|
222
|
+
if (msgRows.length > 0) {
|
|
223
|
+
const parts = await db
|
|
224
|
+
.select()
|
|
225
|
+
.from(messageParts)
|
|
226
|
+
.where(eq(messageParts.messageId, branch.branchPointMessageId))
|
|
227
|
+
.orderBy(asc(messageParts.index));
|
|
228
|
+
|
|
229
|
+
for (const part of parts) {
|
|
230
|
+
if (part.type === 'text') {
|
|
231
|
+
try {
|
|
232
|
+
const content = JSON.parse(part.content || '{}');
|
|
233
|
+
if (content.text) {
|
|
234
|
+
preview = content.text.slice(0, 100);
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
} catch {}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
results.push({
|
|
244
|
+
session: branch,
|
|
245
|
+
branchPointMessageId: branch.branchPointMessageId,
|
|
246
|
+
branchPointPreview: preview,
|
|
247
|
+
createdAt: branch.createdAt,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return results;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function getParentSession(
|
|
255
|
+
db: DB,
|
|
256
|
+
sessionId: string,
|
|
257
|
+
projectPath: string,
|
|
258
|
+
): Promise<SessionRow | null> {
|
|
259
|
+
const sessionRows = await db
|
|
260
|
+
.select()
|
|
261
|
+
.from(sessions)
|
|
262
|
+
.where(eq(sessions.id, sessionId));
|
|
263
|
+
|
|
264
|
+
if (!sessionRows.length) return null;
|
|
265
|
+
|
|
266
|
+
const session = sessionRows[0];
|
|
267
|
+
if (!session.parentSessionId) return null;
|
|
268
|
+
|
|
269
|
+
const parentRows = await db
|
|
270
|
+
.select()
|
|
271
|
+
.from(sessions)
|
|
272
|
+
.where(eq(sessions.id, session.parentSessionId));
|
|
273
|
+
|
|
274
|
+
if (!parentRows.length) return null;
|
|
275
|
+
|
|
276
|
+
const parent = parentRows[0];
|
|
277
|
+
if (parent.projectPath !== projectPath) return null;
|
|
278
|
+
|
|
279
|
+
return parent;
|
|
280
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import type { getDb } from '@ottocode/database';
|
|
2
|
+
import { messages, messageParts, sessions } from '@ottocode/database/schema';
|
|
3
|
+
import { eq } from 'drizzle-orm';
|
|
4
|
+
import { catalog, type ProviderId } from '@ottocode/sdk';
|
|
5
|
+
import type { RunOpts } from './queue.ts';
|
|
6
|
+
|
|
7
|
+
export type UsageData = {
|
|
8
|
+
inputTokens?: number;
|
|
9
|
+
outputTokens?: number;
|
|
10
|
+
totalTokens?: number;
|
|
11
|
+
cachedInputTokens?: number;
|
|
12
|
+
cacheCreationInputTokens?: number;
|
|
13
|
+
reasoningTokens?: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type ProviderMetadata = Record<string, unknown> & {
|
|
17
|
+
openai?: {
|
|
18
|
+
cachedPromptTokens?: number;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
};
|
|
21
|
+
anthropic?: {
|
|
22
|
+
cacheCreationInputTokens?: number;
|
|
23
|
+
cacheReadInputTokens?: number;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function normalizeUsage(
|
|
29
|
+
usage: UsageData,
|
|
30
|
+
providerOptions: ProviderMetadata | undefined,
|
|
31
|
+
provider: ProviderId,
|
|
32
|
+
): UsageData {
|
|
33
|
+
const rawInputTokens = Number(usage.inputTokens ?? 0);
|
|
34
|
+
const outputTokens = Number(usage.outputTokens ?? 0);
|
|
35
|
+
const reasoningTokens = Number(usage.reasoningTokens ?? 0);
|
|
36
|
+
|
|
37
|
+
const cachedInputTokens =
|
|
38
|
+
usage.cachedInputTokens != null
|
|
39
|
+
? Number(usage.cachedInputTokens)
|
|
40
|
+
: providerOptions?.openai?.cachedPromptTokens != null
|
|
41
|
+
? Number(providerOptions.openai.cachedPromptTokens)
|
|
42
|
+
: providerOptions?.anthropic?.cacheReadInputTokens != null
|
|
43
|
+
? Number(providerOptions.anthropic.cacheReadInputTokens)
|
|
44
|
+
: undefined;
|
|
45
|
+
|
|
46
|
+
const cacheCreationInputTokens =
|
|
47
|
+
usage.cacheCreationInputTokens != null
|
|
48
|
+
? Number(usage.cacheCreationInputTokens)
|
|
49
|
+
: providerOptions?.anthropic?.cacheCreationInputTokens != null
|
|
50
|
+
? Number(providerOptions.anthropic.cacheCreationInputTokens)
|
|
51
|
+
: undefined;
|
|
52
|
+
|
|
53
|
+
const cachedValue = cachedInputTokens ?? 0;
|
|
54
|
+
|
|
55
|
+
let inputTokens = rawInputTokens;
|
|
56
|
+
if (provider === 'openai') {
|
|
57
|
+
inputTokens = Math.max(0, rawInputTokens - cachedValue);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
inputTokens,
|
|
62
|
+
outputTokens,
|
|
63
|
+
cachedInputTokens,
|
|
64
|
+
cacheCreationInputTokens,
|
|
65
|
+
reasoningTokens,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function resolveUsageProvider(
|
|
70
|
+
provider: ProviderId,
|
|
71
|
+
model: string,
|
|
72
|
+
): ProviderId {
|
|
73
|
+
if (
|
|
74
|
+
provider !== 'setu' &&
|
|
75
|
+
provider !== 'openrouter' &&
|
|
76
|
+
provider !== 'opencode'
|
|
77
|
+
) {
|
|
78
|
+
return provider;
|
|
79
|
+
}
|
|
80
|
+
const entry = catalog[provider];
|
|
81
|
+
const normalizedModel = model.includes('/') ? model.split('/').at(-1) : model;
|
|
82
|
+
const modelEntry = entry?.models.find(
|
|
83
|
+
(m) => m.id?.toLowerCase() === normalizedModel?.toLowerCase(),
|
|
84
|
+
);
|
|
85
|
+
const npm = modelEntry?.provider?.npm ?? '';
|
|
86
|
+
if (npm.includes('openai')) return 'openai';
|
|
87
|
+
if (npm.includes('anthropic')) return 'anthropic';
|
|
88
|
+
if (npm.includes('google')) return 'google';
|
|
89
|
+
if (npm.includes('zai')) return 'zai';
|
|
90
|
+
return provider;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Updates session token counts after each step.
|
|
95
|
+
* AI SDK v6: onStepFinish.usage is PER-STEP (each step = one API call).
|
|
96
|
+
* We ADD each step's tokens directly to session totals.
|
|
97
|
+
* We also track currentContextTokens = the latest step's full input context.
|
|
98
|
+
*/
|
|
99
|
+
export async function updateSessionTokensIncremental(
|
|
100
|
+
usage: UsageData,
|
|
101
|
+
providerOptions: ProviderMetadata | undefined,
|
|
102
|
+
opts: RunOpts,
|
|
103
|
+
db: Awaited<ReturnType<typeof getDb>>,
|
|
104
|
+
) {
|
|
105
|
+
if (!usage || !db) return;
|
|
106
|
+
|
|
107
|
+
const currentContextTokens = Number(usage.inputTokens ?? 0);
|
|
108
|
+
|
|
109
|
+
const usageProvider = resolveUsageProvider(opts.provider, opts.model);
|
|
110
|
+
const normalizedUsage = normalizeUsage(usage, providerOptions, usageProvider);
|
|
111
|
+
|
|
112
|
+
const stepInput = Number(normalizedUsage.inputTokens ?? 0);
|
|
113
|
+
const stepOutput = Number(normalizedUsage.outputTokens ?? 0);
|
|
114
|
+
const stepCached = Number(normalizedUsage.cachedInputTokens ?? 0);
|
|
115
|
+
const stepCacheCreation = Number(
|
|
116
|
+
normalizedUsage.cacheCreationInputTokens ?? 0,
|
|
117
|
+
);
|
|
118
|
+
const stepReasoning = Number(normalizedUsage.reasoningTokens ?? 0);
|
|
119
|
+
|
|
120
|
+
const sessRows = await db
|
|
121
|
+
.select()
|
|
122
|
+
.from(sessions)
|
|
123
|
+
.where(eq(sessions.id, opts.sessionId));
|
|
124
|
+
|
|
125
|
+
if (sessRows.length === 0 || !sessRows[0]) return;
|
|
126
|
+
|
|
127
|
+
const sess = sessRows[0];
|
|
128
|
+
|
|
129
|
+
await db
|
|
130
|
+
.update(sessions)
|
|
131
|
+
.set({
|
|
132
|
+
totalInputTokens: Number(sess.totalInputTokens ?? 0) + stepInput,
|
|
133
|
+
totalOutputTokens: Number(sess.totalOutputTokens ?? 0) + stepOutput,
|
|
134
|
+
totalCachedTokens: Number(sess.totalCachedTokens ?? 0) + stepCached,
|
|
135
|
+
totalCacheCreationTokens:
|
|
136
|
+
Number(sess.totalCacheCreationTokens ?? 0) + stepCacheCreation,
|
|
137
|
+
totalReasoningTokens:
|
|
138
|
+
Number(sess.totalReasoningTokens ?? 0) + stepReasoning,
|
|
139
|
+
currentContextTokens,
|
|
140
|
+
})
|
|
141
|
+
.where(eq(sessions.id, opts.sessionId));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Updates session token counts after a run completes.
|
|
146
|
+
* @deprecated Use updateSessionTokensIncremental for per-step tracking
|
|
147
|
+
*/
|
|
148
|
+
export async function updateSessionTokens(
|
|
149
|
+
fin: { usage?: { inputTokens?: number; outputTokens?: number } },
|
|
150
|
+
opts: RunOpts,
|
|
151
|
+
db: Awaited<ReturnType<typeof getDb>>,
|
|
152
|
+
) {
|
|
153
|
+
if (!fin.usage || !db) return;
|
|
154
|
+
|
|
155
|
+
const sessRows = await db
|
|
156
|
+
.select()
|
|
157
|
+
.from(sessions)
|
|
158
|
+
.where(eq(sessions.id, opts.sessionId));
|
|
159
|
+
|
|
160
|
+
if (sessRows.length > 0 && sessRows[0]) {
|
|
161
|
+
const row = sessRows[0];
|
|
162
|
+
const priorInput = Number(row.totalInputTokens ?? 0);
|
|
163
|
+
const priorOutput = Number(row.totalOutputTokens ?? 0);
|
|
164
|
+
const nextInput = priorInput + Number(fin.usage.inputTokens ?? 0);
|
|
165
|
+
const nextOutput = priorOutput + Number(fin.usage.outputTokens ?? 0);
|
|
166
|
+
|
|
167
|
+
await db
|
|
168
|
+
.update(sessions)
|
|
169
|
+
.set({
|
|
170
|
+
totalInputTokens: nextInput,
|
|
171
|
+
totalOutputTokens: nextOutput,
|
|
172
|
+
})
|
|
173
|
+
.where(eq(sessions.id, opts.sessionId));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Updates message token counts after each step.
|
|
179
|
+
* AI SDK v6: onStepFinish.usage is PER-STEP. We ADD each step's tokens to message totals.
|
|
180
|
+
*/
|
|
181
|
+
export async function updateMessageTokensIncremental(
|
|
182
|
+
usage: UsageData,
|
|
183
|
+
providerOptions: ProviderMetadata | undefined,
|
|
184
|
+
opts: RunOpts,
|
|
185
|
+
db: Awaited<ReturnType<typeof getDb>>,
|
|
186
|
+
) {
|
|
187
|
+
if (!usage || !db) return;
|
|
188
|
+
|
|
189
|
+
const usageProvider = resolveUsageProvider(opts.provider, opts.model);
|
|
190
|
+
const normalizedUsage = normalizeUsage(usage, providerOptions, usageProvider);
|
|
191
|
+
|
|
192
|
+
const stepInput = Number(normalizedUsage.inputTokens ?? 0);
|
|
193
|
+
const stepOutput = Number(normalizedUsage.outputTokens ?? 0);
|
|
194
|
+
const stepCached = Number(normalizedUsage.cachedInputTokens ?? 0);
|
|
195
|
+
const stepCacheCreation = Number(
|
|
196
|
+
normalizedUsage.cacheCreationInputTokens ?? 0,
|
|
197
|
+
);
|
|
198
|
+
const stepReasoning = Number(normalizedUsage.reasoningTokens ?? 0);
|
|
199
|
+
|
|
200
|
+
const msgRows = await db
|
|
201
|
+
.select()
|
|
202
|
+
.from(messages)
|
|
203
|
+
.where(eq(messages.id, opts.assistantMessageId));
|
|
204
|
+
|
|
205
|
+
if (msgRows.length > 0 && msgRows[0]) {
|
|
206
|
+
const msg = msgRows[0];
|
|
207
|
+
const nextInput = Number(msg.inputTokens ?? 0) + stepInput;
|
|
208
|
+
const nextOutput = Number(msg.outputTokens ?? 0) + stepOutput;
|
|
209
|
+
const nextCached = Number(msg.cachedInputTokens ?? 0) + stepCached;
|
|
210
|
+
const nextCacheCreation =
|
|
211
|
+
Number(msg.cacheCreationInputTokens ?? 0) + stepCacheCreation;
|
|
212
|
+
const nextReasoning = Number(msg.reasoningTokens ?? 0) + stepReasoning;
|
|
213
|
+
|
|
214
|
+
await db
|
|
215
|
+
.update(messages)
|
|
216
|
+
.set({
|
|
217
|
+
inputTokens: nextInput,
|
|
218
|
+
outputTokens: nextOutput,
|
|
219
|
+
totalTokens:
|
|
220
|
+
nextInput +
|
|
221
|
+
nextOutput +
|
|
222
|
+
nextCached +
|
|
223
|
+
nextCacheCreation +
|
|
224
|
+
nextReasoning,
|
|
225
|
+
cachedInputTokens: nextCached,
|
|
226
|
+
cacheCreationInputTokens: nextCacheCreation,
|
|
227
|
+
reasoningTokens: nextReasoning,
|
|
228
|
+
})
|
|
229
|
+
.where(eq(messages.id, opts.assistantMessageId));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Marks an assistant message as complete.
|
|
235
|
+
* Token usage is tracked incrementally via updateMessageTokensIncremental().
|
|
236
|
+
*/
|
|
237
|
+
export async function completeAssistantMessage(
|
|
238
|
+
_fin: {
|
|
239
|
+
usage?: {
|
|
240
|
+
inputTokens?: number;
|
|
241
|
+
outputTokens?: number;
|
|
242
|
+
totalTokens?: number;
|
|
243
|
+
};
|
|
244
|
+
},
|
|
245
|
+
opts: RunOpts,
|
|
246
|
+
db: Awaited<ReturnType<typeof getDb>>,
|
|
247
|
+
) {
|
|
248
|
+
if (!db) return;
|
|
249
|
+
|
|
250
|
+
// Only mark as complete - tokens are already tracked incrementally
|
|
251
|
+
await db
|
|
252
|
+
.update(messages)
|
|
253
|
+
.set({
|
|
254
|
+
status: 'complete',
|
|
255
|
+
completedAt: Date.now(),
|
|
256
|
+
})
|
|
257
|
+
.where(eq(messages.id, opts.assistantMessageId));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Removes empty text parts from an assistant message.
|
|
262
|
+
*/
|
|
263
|
+
export async function cleanupEmptyTextParts(
|
|
264
|
+
opts: RunOpts,
|
|
265
|
+
db: Awaited<ReturnType<typeof getDb>>,
|
|
266
|
+
) {
|
|
267
|
+
if (!db) return;
|
|
268
|
+
|
|
269
|
+
const parts = await db
|
|
270
|
+
.select()
|
|
271
|
+
.from(messageParts)
|
|
272
|
+
.where(eq(messageParts.messageId, opts.assistantMessageId));
|
|
273
|
+
|
|
274
|
+
for (const p of parts) {
|
|
275
|
+
if (p.type === 'text') {
|
|
276
|
+
let t = '';
|
|
277
|
+
try {
|
|
278
|
+
t = JSON.parse(p.content || '{}')?.text || '';
|
|
279
|
+
} catch {}
|
|
280
|
+
if (!t || !t.trim()) {
|
|
281
|
+
await db.delete(messageParts).where(eq(messageParts.id, p.id));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|