@ottocode/server 0.1.225 → 0.1.227
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 +3 -3
- package/src/events/types.ts +2 -0
- package/src/hono-context.d.ts +7 -0
- package/src/index.ts +9 -1
- package/src/openapi/paths/auth.ts +190 -0
- package/src/routes/ask.ts +5 -3
- package/src/routes/auth.ts +388 -4
- package/src/routes/config/agents.ts +5 -3
- package/src/routes/config/defaults.ts +3 -3
- package/src/routes/config/main.ts +5 -3
- package/src/routes/config/models.ts +87 -8
- package/src/routes/config/providers.ts +8 -4
- package/src/routes/config/utils.ts +11 -4
- package/src/routes/terminals.ts +6 -4
- package/src/routes/tunnel.ts +1 -1
- package/src/runtime/agent/oauth-codex-continuation.ts +10 -0
- package/src/runtime/agent/runner-setup.ts +7 -0
- package/src/runtime/agent/runner.ts +37 -11
- package/src/runtime/ask/service.ts +5 -0
- package/src/runtime/errors/api-error.ts +29 -21
- package/src/runtime/message/service.ts +2 -2
- package/src/runtime/provider/copilot.ts +119 -8
- package/src/runtime/provider/oauth-adapter.ts +2 -3
- package/src/runtime/session/branch.ts +11 -1
- package/src/runtime/session/manager.ts +6 -1
- package/src/runtime/stream/step-finish.ts +3 -0
- package/src/runtime/utils/token.ts +3 -0
- package/src/tools/adapter.ts +4 -3
|
@@ -63,7 +63,8 @@ export async function getAuthTypeForProvider(
|
|
|
63
63
|
projectRoot: string,
|
|
64
64
|
): Promise<'api' | 'oauth' | 'wallet' | undefined> {
|
|
65
65
|
if (embeddedConfig?.auth?.[provider]) {
|
|
66
|
-
|
|
66
|
+
const embeddedAuth = embeddedConfig.auth[provider];
|
|
67
|
+
return 'type' in embeddedAuth ? embeddedAuth.type : 'api';
|
|
67
68
|
}
|
|
68
69
|
const auth = await getAuth(provider, projectRoot);
|
|
69
70
|
return auth?.type as 'api' | 'oauth' | 'wallet' | undefined;
|
|
@@ -83,7 +84,9 @@ export async function discoverAllAgents(
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
} catch (err) {
|
|
86
|
-
logger.debug('Failed to load agents.json',
|
|
87
|
+
logger.debug('Failed to load agents.json', {
|
|
88
|
+
error: err instanceof Error ? err.message : String(err),
|
|
89
|
+
});
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
try {
|
|
@@ -98,7 +101,9 @@ export async function discoverAllAgents(
|
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
103
|
} catch (err) {
|
|
101
|
-
logger.debug('Failed to read local agents directory',
|
|
104
|
+
logger.debug('Failed to read local agents directory', {
|
|
105
|
+
error: err instanceof Error ? err.message : String(err),
|
|
106
|
+
});
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
try {
|
|
@@ -113,7 +118,9 @@ export async function discoverAllAgents(
|
|
|
113
118
|
}
|
|
114
119
|
}
|
|
115
120
|
} catch (err) {
|
|
116
|
-
logger.debug('Failed to read global agents directory',
|
|
121
|
+
logger.debug('Failed to read global agents directory', {
|
|
122
|
+
error: err instanceof Error ? err.message : String(err),
|
|
123
|
+
});
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
return Array.from(agentSet).sort();
|
package/src/routes/terminals.ts
CHANGED
|
@@ -87,12 +87,14 @@ export function registerTerminalsRoutes(
|
|
|
87
87
|
return c.json({ error: 'Terminal not found' }, 404);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
const activeTerminal = terminal;
|
|
91
|
+
|
|
90
92
|
return streamSSE(c, async (stream) => {
|
|
91
93
|
logger.debug('SSE stream started for terminal', { id });
|
|
92
94
|
// Send historical buffer first (unless skipHistory is set)
|
|
93
95
|
const skipHistory = c.req.query('skipHistory') === 'true';
|
|
94
96
|
if (!skipHistory) {
|
|
95
|
-
const history =
|
|
97
|
+
const history = activeTerminal.read();
|
|
96
98
|
logger.debug('SSE sending terminal history', {
|
|
97
99
|
id,
|
|
98
100
|
lines: history.length,
|
|
@@ -120,8 +122,8 @@ export function registerTerminalsRoutes(
|
|
|
120
122
|
let finished = false;
|
|
121
123
|
|
|
122
124
|
function cleanup() {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
activeTerminal.removeDataListener(onData);
|
|
126
|
+
activeTerminal.removeExitListener(onExit);
|
|
125
127
|
c.req.raw.signal.removeEventListener('abort', onAbort);
|
|
126
128
|
}
|
|
127
129
|
|
|
@@ -145,7 +147,7 @@ export function registerTerminalsRoutes(
|
|
|
145
147
|
|
|
146
148
|
function onAbort() {
|
|
147
149
|
logger.debug('SSE client disconnected from terminal', {
|
|
148
|
-
id:
|
|
150
|
+
id: activeTerminal.id,
|
|
149
151
|
});
|
|
150
152
|
stream.close();
|
|
151
153
|
finish();
|
package/src/routes/tunnel.ts
CHANGED
|
@@ -59,7 +59,7 @@ export function registerTunnelRoutes(app: Hono) {
|
|
|
59
59
|
|
|
60
60
|
const url = await activeTunnel.start(port, (msg) => {
|
|
61
61
|
progressMessage = msg;
|
|
62
|
-
logger.debug('Tunnel progress
|
|
62
|
+
logger.debug('Tunnel progress', { message: msg });
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
tunnelUrl = url;
|
|
@@ -2,12 +2,14 @@ export type OauthCodexContinuationInput = {
|
|
|
2
2
|
provider: string;
|
|
3
3
|
isOpenAIOAuth: boolean;
|
|
4
4
|
finishObserved: boolean;
|
|
5
|
+
abortedByUser?: boolean;
|
|
5
6
|
continuationCount: number;
|
|
6
7
|
maxContinuations: number;
|
|
7
8
|
finishReason?: string;
|
|
8
9
|
rawFinishReason?: string;
|
|
9
10
|
firstToolSeen: boolean;
|
|
10
11
|
hasTrailingAssistantText: boolean;
|
|
12
|
+
endedWithToolActivity?: boolean;
|
|
11
13
|
droppedPseudoToolText: boolean;
|
|
12
14
|
lastAssistantText: string;
|
|
13
15
|
};
|
|
@@ -68,6 +70,10 @@ export function decideOauthCodexContinuation(
|
|
|
68
70
|
return { shouldContinue: false };
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
if (input.abortedByUser) {
|
|
74
|
+
return { shouldContinue: false, reason: 'aborted-by-user' };
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
if (input.continuationCount >= input.maxContinuations) {
|
|
72
78
|
return { shouldContinue: false, reason: 'max-continuations-reached' };
|
|
73
79
|
}
|
|
@@ -76,6 +82,10 @@ export function decideOauthCodexContinuation(
|
|
|
76
82
|
return { shouldContinue: true, reason: 'truncated' };
|
|
77
83
|
}
|
|
78
84
|
|
|
85
|
+
if (input.endedWithToolActivity) {
|
|
86
|
+
return { shouldContinue: true, reason: 'ended-on-tool-activity' };
|
|
87
|
+
}
|
|
88
|
+
|
|
79
89
|
if (isMissingAssistantSummary(input)) {
|
|
80
90
|
return { shouldContinue: true, reason: 'no-trailing-assistant-text' };
|
|
81
91
|
}
|
|
@@ -211,6 +211,13 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
211
211
|
const providerOptions = { ...adapted.providerOptions };
|
|
212
212
|
let effectiveMaxOutputTokens = maxOutputTokens;
|
|
213
213
|
|
|
214
|
+
if (opts.provider === 'copilot') {
|
|
215
|
+
providerOptions.openai = {
|
|
216
|
+
...((providerOptions.openai as Record<string, unknown>) || {}),
|
|
217
|
+
store: false,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
214
221
|
if (opts.reasoningText) {
|
|
215
222
|
const underlyingProvider = getUnderlyingProviderKey(
|
|
216
223
|
opts.provider,
|
|
@@ -182,11 +182,16 @@ async function runAssistant(opts: RunOpts) {
|
|
|
182
182
|
let _finishObserved = false;
|
|
183
183
|
let _toolActivityObserved = false;
|
|
184
184
|
let _trailingAssistantTextAfterTool = false;
|
|
185
|
+
let _abortedByUser = false;
|
|
186
|
+
let titleGenerationTriggered = false;
|
|
185
187
|
const unsubscribeFinish = subscribe(opts.sessionId, (evt) => {
|
|
186
188
|
if (evt.type === 'tool.call' || evt.type === 'tool.result') {
|
|
187
189
|
_toolActivityObserved = true;
|
|
188
190
|
_trailingAssistantTextAfterTool = false;
|
|
189
191
|
}
|
|
192
|
+
if (evt.type === 'tool.call') {
|
|
193
|
+
triggerTitleGenerationWhenReady();
|
|
194
|
+
}
|
|
190
195
|
if (evt.type !== 'tool.result') return;
|
|
191
196
|
try {
|
|
192
197
|
const name = (evt.payload as { name?: string } | undefined)?.name;
|
|
@@ -221,6 +226,22 @@ async function runAssistant(opts: RunOpts) {
|
|
|
221
226
|
stepIndex += 1;
|
|
222
227
|
return stepIndex;
|
|
223
228
|
};
|
|
229
|
+
const triggerTitleGenerationWhenReady = () => {
|
|
230
|
+
if (titleGenerationTriggered) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
titleGenerationTriggered = true;
|
|
235
|
+
if (!isFirstMessage) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
void triggerDeferredTitleGeneration({
|
|
240
|
+
cfg,
|
|
241
|
+
db,
|
|
242
|
+
sessionId: opts.sessionId,
|
|
243
|
+
});
|
|
244
|
+
};
|
|
224
245
|
|
|
225
246
|
const reasoningStates = new Map<string, ReasoningState>();
|
|
226
247
|
|
|
@@ -232,6 +253,7 @@ async function runAssistant(opts: RunOpts) {
|
|
|
232
253
|
getCurrentPartId,
|
|
233
254
|
updateCurrentPartId,
|
|
234
255
|
updateAccumulated,
|
|
256
|
+
triggerTitleGenerationWhenReady,
|
|
235
257
|
sharedCtx,
|
|
236
258
|
updateSessionTokensIncremental,
|
|
237
259
|
updateMessageTokensIncremental,
|
|
@@ -245,12 +267,19 @@ async function runAssistant(opts: RunOpts) {
|
|
|
245
267
|
runSessionLoop,
|
|
246
268
|
);
|
|
247
269
|
|
|
248
|
-
const
|
|
270
|
+
const baseOnAbort = createAbortHandler(opts, db, getStepIndex, sharedCtx);
|
|
271
|
+
const onAbort = async (event: Parameters<typeof baseOnAbort>[0]) => {
|
|
272
|
+
_abortedByUser = true;
|
|
273
|
+
await baseOnAbort(event);
|
|
274
|
+
};
|
|
249
275
|
|
|
250
276
|
const onFinish = createFinishHandler(opts, db, completeAssistantMessage);
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
277
|
+
const isCopilotResponsesApi =
|
|
278
|
+
opts.provider === 'copilot' && !opts.model.startsWith('gpt-5-mini');
|
|
279
|
+
const stopWhenCondition =
|
|
280
|
+
isOpenAIOAuth || isCopilotResponsesApi
|
|
281
|
+
? stepCountIs(20)
|
|
282
|
+
: hasToolCall('finish');
|
|
254
283
|
|
|
255
284
|
try {
|
|
256
285
|
const result = streamText({
|
|
@@ -307,13 +336,6 @@ async function runAssistant(opts: RunOpts) {
|
|
|
307
336
|
if (!firstDeltaSeen) {
|
|
308
337
|
firstDeltaSeen = true;
|
|
309
338
|
streamStartTimer.end();
|
|
310
|
-
if (isFirstMessage) {
|
|
311
|
-
void triggerDeferredTitleGeneration({
|
|
312
|
-
cfg,
|
|
313
|
-
db,
|
|
314
|
-
sessionId: opts.sessionId,
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
339
|
}
|
|
318
340
|
|
|
319
341
|
if (!currentPartId) {
|
|
@@ -421,16 +443,20 @@ async function runAssistant(opts: RunOpts) {
|
|
|
421
443
|
|
|
422
444
|
const MAX_CONTINUATIONS = 6;
|
|
423
445
|
const continuationCount = opts.continuationCount ?? 0;
|
|
446
|
+
const endedWithToolActivity =
|
|
447
|
+
_toolActivityObserved && !_trailingAssistantTextAfterTool;
|
|
424
448
|
const continuationDecision = decideOauthCodexContinuation({
|
|
425
449
|
provider: opts.provider,
|
|
426
450
|
isOpenAIOAuth,
|
|
427
451
|
finishObserved: _finishObserved,
|
|
452
|
+
abortedByUser: _abortedByUser,
|
|
428
453
|
continuationCount,
|
|
429
454
|
maxContinuations: MAX_CONTINUATIONS,
|
|
430
455
|
finishReason: streamFinishReason,
|
|
431
456
|
rawFinishReason: streamRawFinishReason,
|
|
432
457
|
firstToolSeen: fs,
|
|
433
458
|
hasTrailingAssistantText: _trailingAssistantTextAfterTool,
|
|
459
|
+
endedWithToolActivity,
|
|
434
460
|
droppedPseudoToolText: oauthTextGuard?.dropped ?? false,
|
|
435
461
|
lastAssistantText: latestAssistantText,
|
|
436
462
|
});
|
|
@@ -127,7 +127,12 @@ async function processAskRequest(
|
|
|
127
127
|
google: { enabled: true },
|
|
128
128
|
openrouter: { enabled: true },
|
|
129
129
|
opencode: { enabled: true },
|
|
130
|
+
copilot: { enabled: true },
|
|
130
131
|
setu: { enabled: true },
|
|
132
|
+
zai: { enabled: true },
|
|
133
|
+
'zai-coding': { enabled: true },
|
|
134
|
+
moonshot: { enabled: true },
|
|
135
|
+
minimax: { enabled: true },
|
|
131
136
|
},
|
|
132
137
|
paths: {
|
|
133
138
|
dataDir: `${projectRoot}/.otto`,
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* across all API endpoints.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
|
8
9
|
import { isDebugEnabled } from '../debug/state.ts';
|
|
9
10
|
import { toErrorPayload } from './handling.ts';
|
|
10
11
|
|
|
@@ -16,7 +17,7 @@ export type APIErrorResponse = {
|
|
|
16
17
|
message: string;
|
|
17
18
|
type: string;
|
|
18
19
|
code?: string;
|
|
19
|
-
status?:
|
|
20
|
+
status?: ContentfulStatusCode;
|
|
20
21
|
details?: Record<string, unknown>;
|
|
21
22
|
stack?: string;
|
|
22
23
|
};
|
|
@@ -27,29 +28,33 @@ export type APIErrorResponse = {
|
|
|
27
28
|
*/
|
|
28
29
|
export class APIError extends Error {
|
|
29
30
|
public readonly code?: string;
|
|
30
|
-
public readonly status:
|
|
31
|
+
public readonly status: ContentfulStatusCode;
|
|
31
32
|
public readonly type: string;
|
|
32
33
|
public readonly details?: Record<string, unknown>;
|
|
33
34
|
|
|
34
35
|
constructor(
|
|
35
36
|
message: string,
|
|
36
|
-
options?:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
options?:
|
|
38
|
+
| ContentfulStatusCode
|
|
39
|
+
| {
|
|
40
|
+
code?: string;
|
|
41
|
+
status?: ContentfulStatusCode;
|
|
42
|
+
type?: string;
|
|
43
|
+
details?: Record<string, unknown>;
|
|
44
|
+
cause?: unknown;
|
|
45
|
+
},
|
|
43
46
|
) {
|
|
44
47
|
super(message);
|
|
45
48
|
this.name = 'APIError';
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
this.
|
|
49
|
-
this.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
const normalizedOptions =
|
|
50
|
+
typeof options === 'number' ? { status: options } : options;
|
|
51
|
+
this.code = normalizedOptions?.code;
|
|
52
|
+
this.status = normalizedOptions?.status ?? 500;
|
|
53
|
+
this.type = normalizedOptions?.type ?? 'api_error';
|
|
54
|
+
this.details = normalizedOptions?.details;
|
|
55
|
+
|
|
56
|
+
if (normalizedOptions?.cause) {
|
|
57
|
+
this.cause = normalizedOptions.cause;
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
// Maintain proper stack trace
|
|
@@ -72,7 +77,7 @@ export function serializeError(err: unknown): APIErrorResponse {
|
|
|
72
77
|
// Determine HTTP status code
|
|
73
78
|
// Default to 400 for generic errors (client errors)
|
|
74
79
|
// Only use 500 if explicitly set or for APIError instances without a status
|
|
75
|
-
let status = 400;
|
|
80
|
+
let status: ContentfulStatusCode = 400;
|
|
76
81
|
|
|
77
82
|
// Handle APIError instances first
|
|
78
83
|
if (err instanceof APIError) {
|
|
@@ -80,15 +85,16 @@ export function serializeError(err: unknown): APIErrorResponse {
|
|
|
80
85
|
} else if (err && typeof err === 'object') {
|
|
81
86
|
const errObj = err as Record<string, unknown>;
|
|
82
87
|
if (typeof errObj.status === 'number') {
|
|
83
|
-
status = errObj.status;
|
|
88
|
+
status = errObj.status as ContentfulStatusCode;
|
|
84
89
|
} else if (typeof errObj.statusCode === 'number') {
|
|
85
|
-
status = errObj.statusCode;
|
|
90
|
+
status = errObj.statusCode as ContentfulStatusCode;
|
|
86
91
|
} else if (
|
|
87
92
|
errObj.details &&
|
|
88
93
|
typeof errObj.details === 'object' &&
|
|
89
94
|
typeof (errObj.details as Record<string, unknown>).statusCode === 'number'
|
|
90
95
|
) {
|
|
91
|
-
status = (errObj.details as Record<string, unknown>)
|
|
96
|
+
status = (errObj.details as Record<string, unknown>)
|
|
97
|
+
.statusCode as ContentfulStatusCode;
|
|
92
98
|
}
|
|
93
99
|
}
|
|
94
100
|
|
|
@@ -130,7 +136,9 @@ export function serializeError(err: unknown): APIErrorResponse {
|
|
|
130
136
|
* @param err - The error to convert
|
|
131
137
|
* @returns Tuple of [APIErrorResponse, HTTP status code]
|
|
132
138
|
*/
|
|
133
|
-
export function createErrorResponse(
|
|
139
|
+
export function createErrorResponse(
|
|
140
|
+
err: unknown,
|
|
141
|
+
): [APIErrorResponse, ContentfulStatusCode] {
|
|
134
142
|
const response = serializeError(err);
|
|
135
143
|
return [response, response.error.status ?? 500];
|
|
136
144
|
}
|
|
@@ -284,7 +284,7 @@ async function generateSessionTitle(args: {
|
|
|
284
284
|
return;
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
const provider = sess.provider ?? cfg.defaults.provider;
|
|
287
|
+
const provider = (sess.provider ?? cfg.defaults.provider) as ProviderId;
|
|
288
288
|
const modelName = sess.model ?? cfg.defaults.model;
|
|
289
289
|
|
|
290
290
|
debugLog('[TITLE_GEN] Generating title for session');
|
|
@@ -365,7 +365,7 @@ Output ONLY the title, nothing else.`;
|
|
|
365
365
|
|
|
366
366
|
await db
|
|
367
367
|
.update(sessions)
|
|
368
|
-
.set({ title: sanitized,
|
|
368
|
+
.set({ title: sanitized, lastActiveAt: Date.now() })
|
|
369
369
|
.where(eq(sessions.id, sessionId));
|
|
370
370
|
|
|
371
371
|
debugLog(`[TITLE_GEN] Setting final title: "${sanitized}"`);
|
|
@@ -1,12 +1,123 @@
|
|
|
1
|
-
import { getAuth, createCopilotModel } from '@ottocode/sdk';
|
|
2
|
-
import type { OttoConfig } from '@ottocode/sdk';
|
|
1
|
+
import { getAuth, createCopilotModel, readEnvKey } from '@ottocode/sdk';
|
|
2
|
+
import type { OttoConfig, OAuth } from '@ottocode/sdk';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
const COPILOT_MODELS_URL = 'https://api.githubcopilot.com/models';
|
|
5
|
+
const COPILOT_MODELS_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
6
|
+
|
|
7
|
+
type CachedCopilotModels = {
|
|
8
|
+
expiresAt: number;
|
|
9
|
+
models: Set<string>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const copilotModelsCache = new Map<string, CachedCopilotModels>();
|
|
13
|
+
|
|
14
|
+
type CopilotTokenCandidate = {
|
|
15
|
+
source: 'env' | 'oauth';
|
|
16
|
+
token: string;
|
|
17
|
+
oauth: OAuth;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
async function getCopilotTokenCandidates(
|
|
21
|
+
projectRoot: string,
|
|
22
|
+
): Promise<CopilotTokenCandidate[]> {
|
|
23
|
+
const candidates: CopilotTokenCandidate[] = [];
|
|
24
|
+
|
|
25
|
+
const envToken = readEnvKey('copilot');
|
|
26
|
+
if (envToken) {
|
|
27
|
+
candidates.push({
|
|
28
|
+
source: 'env',
|
|
29
|
+
token: envToken,
|
|
30
|
+
oauth: {
|
|
31
|
+
type: 'oauth',
|
|
32
|
+
access: envToken,
|
|
33
|
+
refresh: envToken,
|
|
34
|
+
expires: 0,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const auth = await getAuth('copilot', projectRoot);
|
|
6
40
|
if (auth?.type === 'oauth') {
|
|
7
|
-
|
|
41
|
+
if (auth.refresh !== envToken) {
|
|
42
|
+
candidates.push({ source: 'oauth', token: auth.refresh, oauth: auth });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return candidates;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function getCopilotAvailableModels(
|
|
50
|
+
token: string,
|
|
51
|
+
): Promise<Set<string> | null> {
|
|
52
|
+
const cached = copilotModelsCache.get(token);
|
|
53
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
54
|
+
return cached.models;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const response = await fetch(COPILOT_MODELS_URL, {
|
|
59
|
+
headers: {
|
|
60
|
+
Authorization: `Bearer ${token}`,
|
|
61
|
+
'Openai-Intent': 'conversation-edits',
|
|
62
|
+
'User-Agent': 'ottocode',
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (!response.ok) return null;
|
|
67
|
+
|
|
68
|
+
const payload = (await response.json()) as {
|
|
69
|
+
data?: Array<{ id?: string }>;
|
|
70
|
+
};
|
|
71
|
+
const models = new Set(
|
|
72
|
+
(payload.data ?? [])
|
|
73
|
+
.map((item) => item.id)
|
|
74
|
+
.filter((id): id is string => Boolean(id)),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
copilotModelsCache.set(token, {
|
|
78
|
+
expiresAt: Date.now() + COPILOT_MODELS_CACHE_TTL_MS,
|
|
79
|
+
models,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return models;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function resolveCopilotModel(model: string, cfg: OttoConfig) {
|
|
89
|
+
const candidates = await getCopilotTokenCandidates(cfg.projectRoot);
|
|
90
|
+
if (!candidates.length) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
'Copilot provider requires OAuth or GITHUB_TOKEN. Run `otto auth login copilot` or set GITHUB_TOKEN.',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let selected: CopilotTokenCandidate | null = null;
|
|
97
|
+
const unionAvailableModels = new Set<string>();
|
|
98
|
+
|
|
99
|
+
for (const candidate of candidates) {
|
|
100
|
+
const availableModels = await getCopilotAvailableModels(candidate.token);
|
|
101
|
+
if (!availableModels || availableModels.size === 0) continue;
|
|
102
|
+
|
|
103
|
+
for (const availableModel of availableModels) {
|
|
104
|
+
unionAvailableModels.add(availableModel);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!selected && availableModels.has(model)) {
|
|
108
|
+
selected = candidate;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (selected) {
|
|
113
|
+
return createCopilotModel(model, { oauth: selected.oauth });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (unionAvailableModels.size > 0) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Copilot model '${model}' is not available for this account/organization token. Available models: ${Array.from(unionAvailableModels).join(', ')}`,
|
|
119
|
+
);
|
|
8
120
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
);
|
|
121
|
+
|
|
122
|
+
return createCopilotModel(model, { oauth: candidates[0].oauth });
|
|
12
123
|
}
|
|
@@ -69,11 +69,10 @@ export function detectOAuth(
|
|
|
69
69
|
): OAuthContext {
|
|
70
70
|
const isOAuth = auth?.type === 'oauth';
|
|
71
71
|
const needsSpoof = !!isOAuth && provider === 'anthropic';
|
|
72
|
-
const isCopilot = provider === 'copilot';
|
|
73
72
|
return {
|
|
74
|
-
isOAuth: !!isOAuth
|
|
73
|
+
isOAuth: !!isOAuth,
|
|
75
74
|
needsSpoof,
|
|
76
|
-
isOpenAIOAuth:
|
|
75
|
+
isOpenAIOAuth: !!isOAuth && provider === 'openai',
|
|
77
76
|
spoofPrompt: needsSpoof ? getProviderSpoofPrompt(provider) : undefined,
|
|
78
77
|
};
|
|
79
78
|
}
|
|
@@ -160,7 +160,14 @@ export async function createBranch({
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
const result: SessionRow = {
|
|
163
|
-
|
|
163
|
+
id: newSession.id,
|
|
164
|
+
title: newSession.title ?? null,
|
|
165
|
+
agent: newSession.agent,
|
|
166
|
+
provider: newSession.provider,
|
|
167
|
+
model: newSession.model,
|
|
168
|
+
projectPath: newSession.projectPath,
|
|
169
|
+
createdAt: newSession.createdAt,
|
|
170
|
+
lastActiveAt: newSession.lastActiveAt ?? null,
|
|
164
171
|
totalInputTokens: null,
|
|
165
172
|
totalOutputTokens: null,
|
|
166
173
|
totalCachedTokens: null,
|
|
@@ -171,6 +178,9 @@ export async function createBranch({
|
|
|
171
178
|
currentContextTokens: null,
|
|
172
179
|
contextSummary: null,
|
|
173
180
|
lastCompactedAt: null,
|
|
181
|
+
parentSessionId: newSession.parentSessionId ?? null,
|
|
182
|
+
branchPointMessageId: newSession.branchPointMessageId ?? null,
|
|
183
|
+
sessionType: newSession.sessionType ?? null,
|
|
174
184
|
};
|
|
175
185
|
|
|
176
186
|
publish({
|
|
@@ -39,7 +39,7 @@ export async function createSession({
|
|
|
39
39
|
await ensureProviderEnv(cfg, provider);
|
|
40
40
|
const id = crypto.randomUUID();
|
|
41
41
|
const now = Date.now();
|
|
42
|
-
const row = {
|
|
42
|
+
const row: SessionRow = {
|
|
43
43
|
id,
|
|
44
44
|
title: title ?? null,
|
|
45
45
|
agent,
|
|
@@ -56,6 +56,11 @@ export async function createSession({
|
|
|
56
56
|
totalToolTimeMs: null,
|
|
57
57
|
toolCountsJson: null,
|
|
58
58
|
currentContextTokens: null,
|
|
59
|
+
contextSummary: null,
|
|
60
|
+
lastCompactedAt: null,
|
|
61
|
+
parentSessionId: null,
|
|
62
|
+
branchPointMessageId: null,
|
|
63
|
+
sessionType: 'main',
|
|
59
64
|
};
|
|
60
65
|
await db.insert(sessions).values(row);
|
|
61
66
|
publish({ type: 'session.created', sessionId: id, payload: row });
|
|
@@ -16,6 +16,7 @@ export function createStepFinishHandler(
|
|
|
16
16
|
getCurrentPartId: () => string | null,
|
|
17
17
|
updateCurrentPartId: (id: string | null) => void,
|
|
18
18
|
updateAccumulated: (text: string) => void,
|
|
19
|
+
triggerTitleGenerationWhenReady: () => void,
|
|
19
20
|
sharedCtx: ToolAdapterContext,
|
|
20
21
|
updateSessionTokensIncrementalFn: (
|
|
21
22
|
usage: UsageData,
|
|
@@ -31,6 +32,8 @@ export function createStepFinishHandler(
|
|
|
31
32
|
) => Promise<void>,
|
|
32
33
|
) {
|
|
33
34
|
return async (step: StepFinishEvent) => {
|
|
35
|
+
triggerTitleGenerationWhenReady();
|
|
36
|
+
|
|
34
37
|
const finishedAt = Date.now();
|
|
35
38
|
const currentPartId = getCurrentPartId();
|
|
36
39
|
const stepIndex = getStepIndex();
|
package/src/tools/adapter.ts
CHANGED
|
@@ -130,12 +130,13 @@ export function adaptTools(
|
|
|
130
130
|
}: {
|
|
131
131
|
callId?: string;
|
|
132
132
|
startTs?: number;
|
|
133
|
-
stepIndexForEvent
|
|
133
|
+
stepIndexForEvent?: number;
|
|
134
134
|
args?: unknown;
|
|
135
135
|
},
|
|
136
136
|
) => {
|
|
137
137
|
const resultPartId = crypto.randomUUID();
|
|
138
138
|
const endTs = Date.now();
|
|
139
|
+
const effectiveStepIndex = stepIndexForEvent ?? ctx.stepIndex;
|
|
139
140
|
const dur =
|
|
140
141
|
typeof startTs === 'number' ? Math.max(0, endTs - startTs) : null;
|
|
141
142
|
|
|
@@ -160,7 +161,7 @@ export function adaptTools(
|
|
|
160
161
|
id: resultPartId,
|
|
161
162
|
messageId: ctx.messageId,
|
|
162
163
|
index,
|
|
163
|
-
stepIndex:
|
|
164
|
+
stepIndex: effectiveStepIndex,
|
|
164
165
|
type: 'tool_result',
|
|
165
166
|
content: JSON.stringify(contentObj),
|
|
166
167
|
agent: ctx.agent,
|
|
@@ -176,7 +177,7 @@ export function adaptTools(
|
|
|
176
177
|
publish({
|
|
177
178
|
type: 'tool.result',
|
|
178
179
|
sessionId: ctx.sessionId,
|
|
179
|
-
payload: { ...contentObj, stepIndex:
|
|
180
|
+
payload: { ...contentObj, stepIndex: effectiveStepIndex },
|
|
180
181
|
});
|
|
181
182
|
};
|
|
182
183
|
|