@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,167 @@
|
|
|
1
|
+
import { providerBasePrompt } from '@ottocode/sdk';
|
|
2
|
+
import { debugLog } from '../debug/index.ts';
|
|
3
|
+
import { composeEnvironmentAndInstructions } from '../context/environment.ts';
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
5
|
+
import BASE_PROMPT from '@ottocode/sdk/prompts/base.txt' with { type: 'text' };
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
7
|
+
import ONESHOT_PROMPT from '@ottocode/sdk/prompts/modes/oneshot.txt' with {
|
|
8
|
+
type: 'text',
|
|
9
|
+
};
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
11
|
+
import ANTHROPIC_SPOOF_PROMPT from '@ottocode/sdk/prompts/providers/anthropicSpoof.txt' with {
|
|
12
|
+
type: 'text',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
import { getTerminalManager } from '@ottocode/sdk';
|
|
16
|
+
|
|
17
|
+
export type ComposedSystemPrompt = {
|
|
18
|
+
prompt: string;
|
|
19
|
+
components: string[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export async function composeSystemPrompt(options: {
|
|
23
|
+
provider: string;
|
|
24
|
+
model?: string;
|
|
25
|
+
projectRoot: string;
|
|
26
|
+
agentPrompt: string;
|
|
27
|
+
oneShot?: boolean;
|
|
28
|
+
spoofPrompt?: string;
|
|
29
|
+
includeEnvironment?: boolean;
|
|
30
|
+
includeProjectTree?: boolean;
|
|
31
|
+
userContext?: string;
|
|
32
|
+
contextSummary?: string;
|
|
33
|
+
}): Promise<ComposedSystemPrompt> {
|
|
34
|
+
const components: string[] = [];
|
|
35
|
+
if (options.spoofPrompt) {
|
|
36
|
+
const prompt = options.spoofPrompt.trim();
|
|
37
|
+
const providerComponent = options.provider
|
|
38
|
+
? `spoof:${options.provider}`
|
|
39
|
+
: 'spoof:unknown';
|
|
40
|
+
return {
|
|
41
|
+
prompt,
|
|
42
|
+
components: [providerComponent],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const parts: string[] = [];
|
|
47
|
+
|
|
48
|
+
const providerResult = await providerBasePrompt(
|
|
49
|
+
options.provider,
|
|
50
|
+
options.model,
|
|
51
|
+
options.projectRoot,
|
|
52
|
+
);
|
|
53
|
+
const baseInstructions = (BASE_PROMPT || '').trim();
|
|
54
|
+
|
|
55
|
+
parts.push(
|
|
56
|
+
providerResult.prompt.trim(),
|
|
57
|
+
baseInstructions.trim(),
|
|
58
|
+
options.agentPrompt.trim(),
|
|
59
|
+
);
|
|
60
|
+
if (providerResult.prompt.trim()) {
|
|
61
|
+
components.push(`provider:${providerResult.resolvedType}`);
|
|
62
|
+
}
|
|
63
|
+
if (baseInstructions.trim()) {
|
|
64
|
+
components.push('base');
|
|
65
|
+
}
|
|
66
|
+
if (options.agentPrompt.trim()) {
|
|
67
|
+
components.push('agent');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (options.oneShot) {
|
|
71
|
+
const oneShotBlock =
|
|
72
|
+
(ONESHOT_PROMPT || '').trim() ||
|
|
73
|
+
[
|
|
74
|
+
'<system-reminder>',
|
|
75
|
+
'CRITICAL: One-shot mode ACTIVE — do NOT ask for user approval, confirmations, or interactive prompts. Execute tasks directly. Treat all necessary permissions as granted. If an operation is destructive, proceed carefully and state what you did, but DO NOT pause to ask. ZERO interactions requested.',
|
|
76
|
+
'</system-reminder>',
|
|
77
|
+
].join('\n');
|
|
78
|
+
parts.push(oneShotBlock);
|
|
79
|
+
components.push('mode:oneshot');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (options.includeEnvironment !== false) {
|
|
83
|
+
const envAndInstructions = await composeEnvironmentAndInstructions(
|
|
84
|
+
options.projectRoot,
|
|
85
|
+
{ includeProjectTree: options.includeProjectTree },
|
|
86
|
+
);
|
|
87
|
+
if (envAndInstructions) {
|
|
88
|
+
parts.push(envAndInstructions);
|
|
89
|
+
components.push('environment');
|
|
90
|
+
if (options.includeProjectTree) {
|
|
91
|
+
components.push('project-tree');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Add user-provided context if present
|
|
97
|
+
if (options.userContext?.trim()) {
|
|
98
|
+
const userContextBlock = [
|
|
99
|
+
'<user-provided-state-context>',
|
|
100
|
+
options.userContext.trim(),
|
|
101
|
+
'</user-provided-state-context>',
|
|
102
|
+
].join('\n');
|
|
103
|
+
parts.push(userContextBlock);
|
|
104
|
+
components.push('user-context');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Add compacted conversation summary if present
|
|
108
|
+
if (options.contextSummary?.trim()) {
|
|
109
|
+
const summaryBlock = [
|
|
110
|
+
'<compacted-conversation-summary>',
|
|
111
|
+
'The conversation was compacted to save context. Here is a summary of the previous context:',
|
|
112
|
+
'',
|
|
113
|
+
options.contextSummary.trim(),
|
|
114
|
+
'</compacted-conversation-summary>',
|
|
115
|
+
].join('\n');
|
|
116
|
+
parts.push(summaryBlock);
|
|
117
|
+
components.push('context-summary');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Add terminal context if available
|
|
121
|
+
const terminalManager = getTerminalManager();
|
|
122
|
+
if (terminalManager) {
|
|
123
|
+
const terminalContext = terminalManager.getContext();
|
|
124
|
+
if (terminalContext) {
|
|
125
|
+
parts.push(terminalContext);
|
|
126
|
+
components.push('terminal-context');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const composed = parts.filter(Boolean).join('\n\n').trim();
|
|
131
|
+
if (composed) {
|
|
132
|
+
debugLog(`[system] pieces: ${dedupeComponents(components).join(', ')}`);
|
|
133
|
+
return {
|
|
134
|
+
prompt: composed,
|
|
135
|
+
components: dedupeComponents(components),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const fallback = [
|
|
140
|
+
'You are a concise, friendly coding agent.',
|
|
141
|
+
'Be precise and actionable. Use tools when needed, prefer small diffs.',
|
|
142
|
+
'Stream your answer; call finish when done.',
|
|
143
|
+
].join(' ');
|
|
144
|
+
return {
|
|
145
|
+
prompt: fallback,
|
|
146
|
+
components: dedupeComponents([...components, 'fallback']),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function getProviderSpoofPrompt(provider: string): string | undefined {
|
|
151
|
+
if (provider === 'anthropic') {
|
|
152
|
+
return (ANTHROPIC_SPOOF_PROMPT || '').trim();
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function dedupeComponents(input: string[]): string[] {
|
|
158
|
+
const seen = new Set<string>();
|
|
159
|
+
const out: string[] = [];
|
|
160
|
+
for (const item of input) {
|
|
161
|
+
if (!item) continue;
|
|
162
|
+
if (seen.has(item)) continue;
|
|
163
|
+
seen.add(item);
|
|
164
|
+
out.push(item);
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { OttoConfig } from '@ottocode/sdk';
|
|
2
|
+
import {
|
|
3
|
+
getAuth,
|
|
4
|
+
refreshToken,
|
|
5
|
+
setAuth,
|
|
6
|
+
createAnthropicOAuthModel,
|
|
7
|
+
createAnthropicCachingFetch,
|
|
8
|
+
} from '@ottocode/sdk';
|
|
9
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
10
|
+
import { toClaudeCodeName } from '../tools/mapping.ts';
|
|
11
|
+
|
|
12
|
+
export async function getAnthropicInstance(cfg: OttoConfig) {
|
|
13
|
+
const auth = await getAuth('anthropic', cfg.projectRoot);
|
|
14
|
+
|
|
15
|
+
if (auth?.type === 'oauth') {
|
|
16
|
+
let currentAuth = auth;
|
|
17
|
+
|
|
18
|
+
if (currentAuth.expires < Date.now()) {
|
|
19
|
+
const tokens = await refreshToken(currentAuth.refresh);
|
|
20
|
+
await setAuth(
|
|
21
|
+
'anthropic',
|
|
22
|
+
{
|
|
23
|
+
type: 'oauth',
|
|
24
|
+
refresh: tokens.refresh,
|
|
25
|
+
access: tokens.access,
|
|
26
|
+
expires: tokens.expires,
|
|
27
|
+
},
|
|
28
|
+
cfg.projectRoot,
|
|
29
|
+
'global',
|
|
30
|
+
);
|
|
31
|
+
currentAuth = {
|
|
32
|
+
type: 'oauth',
|
|
33
|
+
refresh: tokens.refresh,
|
|
34
|
+
access: tokens.access,
|
|
35
|
+
expires: tokens.expires,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (model: string) =>
|
|
40
|
+
createAnthropicOAuthModel(model, {
|
|
41
|
+
oauth: currentAuth,
|
|
42
|
+
toolNameTransformer: toClaudeCodeName,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const cachingFetch = createAnthropicCachingFetch();
|
|
47
|
+
return createAnthropic({
|
|
48
|
+
fetch: cachingFetch as typeof fetch,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { getAuth, createCopilotModel } from '@ottocode/sdk';
|
|
2
|
+
import type { OttoConfig } from '@ottocode/sdk';
|
|
3
|
+
|
|
4
|
+
export async function resolveCopilotModel(model: string, cfg: OttoConfig) {
|
|
5
|
+
const auth = await getAuth('copilot', cfg.projectRoot);
|
|
6
|
+
if (auth?.type === 'oauth') {
|
|
7
|
+
return createCopilotModel(model, { oauth: auth });
|
|
8
|
+
}
|
|
9
|
+
throw new Error(
|
|
10
|
+
'Copilot provider requires OAuth. Run `otto auth login copilot`.',
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { OttoConfig } from '@ottocode/sdk';
|
|
2
|
+
import { getAuth, createGoogleModel } from '@ottocode/sdk';
|
|
3
|
+
|
|
4
|
+
export async function resolveGoogleModel(model: string, cfg: OttoConfig) {
|
|
5
|
+
const auth = await getAuth('google', cfg.projectRoot);
|
|
6
|
+
const apiKey = auth?.type === 'api' ? auth.key : undefined;
|
|
7
|
+
return createGoogleModel(model, { apiKey });
|
|
8
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { OttoConfig, ProviderId } from '@ottocode/sdk';
|
|
2
|
+
import { getAnthropicInstance } from './anthropic.ts';
|
|
3
|
+
import { resolveOpenAIModel } from './openai.ts';
|
|
4
|
+
import { resolveGoogleModel } from './google.ts';
|
|
5
|
+
import { resolveOpenRouterModel } from './openrouter.ts';
|
|
6
|
+
import { resolveSetuModel, type ResolveSetuModelOptions } from './setu.ts';
|
|
7
|
+
import { getZaiInstance, getZaiCodingInstance } from './zai.ts';
|
|
8
|
+
import { resolveOpencodeModel } from './opencode.ts';
|
|
9
|
+
import { getMoonshotInstance } from './moonshot.ts';
|
|
10
|
+
import { resolveCopilotModel } from './copilot.ts';
|
|
11
|
+
|
|
12
|
+
export type ProviderName = ProviderId;
|
|
13
|
+
|
|
14
|
+
export async function resolveModel(
|
|
15
|
+
provider: ProviderName,
|
|
16
|
+
model: string,
|
|
17
|
+
cfg: OttoConfig,
|
|
18
|
+
options?: {
|
|
19
|
+
systemPrompt?: string;
|
|
20
|
+
sessionId?: string;
|
|
21
|
+
messageId?: string;
|
|
22
|
+
topupApprovalMode?: ResolveSetuModelOptions['topupApprovalMode'];
|
|
23
|
+
},
|
|
24
|
+
) {
|
|
25
|
+
if (provider === 'openai') {
|
|
26
|
+
return resolveOpenAIModel(model, cfg);
|
|
27
|
+
}
|
|
28
|
+
if (provider === 'anthropic') {
|
|
29
|
+
const instance = await getAnthropicInstance(cfg);
|
|
30
|
+
return instance(model);
|
|
31
|
+
}
|
|
32
|
+
if (provider === 'google') {
|
|
33
|
+
return resolveGoogleModel(model, cfg);
|
|
34
|
+
}
|
|
35
|
+
if (provider === 'openrouter') {
|
|
36
|
+
return resolveOpenRouterModel(model);
|
|
37
|
+
}
|
|
38
|
+
if (provider === 'opencode') {
|
|
39
|
+
return resolveOpencodeModel(model, cfg);
|
|
40
|
+
}
|
|
41
|
+
if (provider === 'copilot') {
|
|
42
|
+
return resolveCopilotModel(model, cfg);
|
|
43
|
+
}
|
|
44
|
+
if (provider === 'setu') {
|
|
45
|
+
return await resolveSetuModel(model, options?.sessionId, {
|
|
46
|
+
messageId: options?.messageId,
|
|
47
|
+
topupApprovalMode: options?.topupApprovalMode,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (provider === 'zai') {
|
|
51
|
+
return getZaiInstance(cfg, model);
|
|
52
|
+
}
|
|
53
|
+
if (provider === 'zai-coding') {
|
|
54
|
+
return getZaiCodingInstance(cfg, model);
|
|
55
|
+
}
|
|
56
|
+
if (provider === 'moonshot') {
|
|
57
|
+
return getMoonshotInstance(cfg, model);
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`Unsupported provider: ${provider}`);
|
|
60
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { OttoConfig } from '@ottocode/sdk';
|
|
2
|
+
import { getAuth, createMoonshotModel } from '@ottocode/sdk';
|
|
3
|
+
|
|
4
|
+
export async function getMoonshotInstance(cfg: OttoConfig, model: string) {
|
|
5
|
+
const auth = await getAuth('moonshot', cfg.projectRoot);
|
|
6
|
+
const apiKey = auth?.type === 'api' ? auth.key : undefined;
|
|
7
|
+
return createMoonshotModel(model, { apiKey });
|
|
8
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Provider Adapter
|
|
3
|
+
*
|
|
4
|
+
* Consolidates all OAuth-specific LLM call adaptations into one place.
|
|
5
|
+
* Each OAuth provider has quirks (no system prompt, must stream, needs
|
|
6
|
+
* spoof prompt, special providerOptions, etc.). Instead of duplicating
|
|
7
|
+
* that branching logic across every callsite (title gen, compaction,
|
|
8
|
+
* commit, runner), this module exposes two layers:
|
|
9
|
+
*
|
|
10
|
+
* ## Layer 1 — Detection (`detectOAuth`)
|
|
11
|
+
* Examines provider + auth and returns an `OAuthContext` describing
|
|
12
|
+
* what adaptations are needed. Used by ALL callsites (simple + complex).
|
|
13
|
+
*
|
|
14
|
+
* ## Layer 2 — Simple call adaptation (`adaptSimpleCall`)
|
|
15
|
+
* For single-shot LLM calls (title gen, compaction, commit) that follow
|
|
16
|
+
* the pattern: system + user message → text result.
|
|
17
|
+
* Returns a ready-to-spread `AdaptedLLMCall` object.
|
|
18
|
+
*
|
|
19
|
+
* ## Adding a new OAuth provider
|
|
20
|
+
* 1. Add detection branch in `detectOAuth()`
|
|
21
|
+
* 2. Add adaptation branch in `adaptSimpleCall()`
|
|
22
|
+
* 3. If the provider needs a custom fetch wrapper, add it under
|
|
23
|
+
* `packages/sdk/src/providers/src/<provider>-oauth-client.ts`
|
|
24
|
+
* 4. Zero changes needed at any callsite.
|
|
25
|
+
*
|
|
26
|
+
* ## Architecture
|
|
27
|
+
*
|
|
28
|
+
* ```
|
|
29
|
+
* callsite (commit.ts, service.ts, compaction-auto.ts, runner-setup.ts)
|
|
30
|
+
* │
|
|
31
|
+
* ├─ detectOAuth(provider, auth) → OAuthContext
|
|
32
|
+
* │
|
|
33
|
+
* ├─ adaptSimpleCall(ctx, input) → AdaptedLLMCall (title, commit, compaction)
|
|
34
|
+
* │
|
|
35
|
+
* └─ adaptRunnerCall(ctx, composed, opts) → AdaptedRunnerSetup (main chat)
|
|
36
|
+
* │
|
|
37
|
+
* ├─ OpenAI OAuth (Codex): no system, inline instructions,
|
|
38
|
+
* │ providerOptions.openai.store=false, forceStream=true
|
|
39
|
+
* │
|
|
40
|
+
* ├─ Anthropic OAuth: spoofPrompt as system, instructions
|
|
41
|
+
* │ folded into user message, normal maxOutputTokens
|
|
42
|
+
* │
|
|
43
|
+
* └─ API key (default): system=instructions, plain user msg
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
import { getProviderSpoofPrompt } from '../prompt/builder.ts';
|
|
47
|
+
import type { SharedV3ProviderOptions } from '@ai-sdk/provider';
|
|
48
|
+
|
|
49
|
+
export type OAuthContext = {
|
|
50
|
+
isOAuth: boolean;
|
|
51
|
+
needsSpoof: boolean;
|
|
52
|
+
isOpenAIOAuth: boolean;
|
|
53
|
+
spoofPrompt: string | undefined;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Detect OAuth mode for a provider and return flags describing
|
|
58
|
+
* what adaptations are needed. This replaces the 4-line pattern
|
|
59
|
+
* that was previously copy-pasted at every callsite:
|
|
60
|
+
*
|
|
61
|
+
* const isOAuth = auth?.type === 'oauth';
|
|
62
|
+
* const needsSpoof = isOAuth && provider === 'anthropic';
|
|
63
|
+
* const isOpenAIOAuth = isOAuth && provider === 'openai';
|
|
64
|
+
* const spoofPrompt = needsSpoof ? getProviderSpoofPrompt(...) : undefined;
|
|
65
|
+
*/
|
|
66
|
+
export function detectOAuth(
|
|
67
|
+
provider: string,
|
|
68
|
+
auth: { type: string } | null | undefined,
|
|
69
|
+
): OAuthContext {
|
|
70
|
+
const isOAuth = auth?.type === 'oauth';
|
|
71
|
+
const needsSpoof = !!isOAuth && provider === 'anthropic';
|
|
72
|
+
const isCopilot = provider === 'copilot';
|
|
73
|
+
return {
|
|
74
|
+
isOAuth: !!isOAuth || isCopilot,
|
|
75
|
+
needsSpoof,
|
|
76
|
+
isOpenAIOAuth: (!!isOAuth && provider === 'openai') || isCopilot,
|
|
77
|
+
spoofPrompt: needsSpoof ? getProviderSpoofPrompt(provider) : undefined,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build OpenAI Codex-specific providerOptions.
|
|
83
|
+
* Codex requires `store: false` and passes the system prompt via
|
|
84
|
+
* `instructions` instead of the normal `system` field.
|
|
85
|
+
*
|
|
86
|
+
* Used directly by runner-setup.ts (complex flow) and indirectly
|
|
87
|
+
* by adaptSimpleCall (simple flows).
|
|
88
|
+
*/
|
|
89
|
+
export function buildCodexProviderOptions(instructions: string) {
|
|
90
|
+
return {
|
|
91
|
+
openai: { store: false as const, instructions },
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type AdaptedLLMCall = {
|
|
96
|
+
system?: string;
|
|
97
|
+
messages: Array<{ role: 'user'; content: string }>;
|
|
98
|
+
maxOutputTokens?: number;
|
|
99
|
+
providerOptions?: SharedV3ProviderOptions;
|
|
100
|
+
forceStream: boolean;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Adapt a simple (single-shot) LLM call for the current OAuth context.
|
|
105
|
+
*
|
|
106
|
+
* Takes raw `instructions` (what would normally be the system prompt) and
|
|
107
|
+
* `userContent`, then returns the correct shape for the provider:
|
|
108
|
+
*
|
|
109
|
+
* - **OpenAI OAuth (Codex)**: no system prompt, instructions baked into
|
|
110
|
+
* user message AND providerOptions.openai.instructions, forceStream=true,
|
|
111
|
+
* no maxOutputTokens (Codex doesn't support it).
|
|
112
|
+
*
|
|
113
|
+
* - **Anthropic OAuth**: spoof prompt as system, real instructions folded
|
|
114
|
+
* into user message, normal maxOutputTokens.
|
|
115
|
+
*
|
|
116
|
+
* - **API key (default)**: instructions as system, plain user message,
|
|
117
|
+
* normal maxOutputTokens.
|
|
118
|
+
*
|
|
119
|
+
* Callsites just spread the result into streamText/generateText:
|
|
120
|
+
* ```ts
|
|
121
|
+
* const adapted = adaptSimpleCall(oauth, { instructions, userContent });
|
|
122
|
+
* const result = streamText({ model, ...adapted }); // almost — see forceStream
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
export function adaptSimpleCall(
|
|
126
|
+
ctx: OAuthContext,
|
|
127
|
+
input: {
|
|
128
|
+
instructions: string;
|
|
129
|
+
userContent: string;
|
|
130
|
+
maxOutputTokens?: number;
|
|
131
|
+
},
|
|
132
|
+
): AdaptedLLMCall {
|
|
133
|
+
if (ctx.isOpenAIOAuth) {
|
|
134
|
+
return {
|
|
135
|
+
messages: [
|
|
136
|
+
{
|
|
137
|
+
role: 'user',
|
|
138
|
+
content: `${input.instructions}\n\n${input.userContent}`,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
providerOptions: buildCodexProviderOptions(input.instructions),
|
|
142
|
+
forceStream: true,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (ctx.needsSpoof && ctx.spoofPrompt) {
|
|
147
|
+
return {
|
|
148
|
+
system: ctx.spoofPrompt,
|
|
149
|
+
messages: [
|
|
150
|
+
{
|
|
151
|
+
role: 'user',
|
|
152
|
+
content: `${input.instructions}\n\n${input.userContent}`,
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
maxOutputTokens: input.maxOutputTokens,
|
|
156
|
+
forceStream: false,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
system: input.instructions,
|
|
162
|
+
messages: [{ role: 'user', content: input.userContent }],
|
|
163
|
+
maxOutputTokens: input.maxOutputTokens,
|
|
164
|
+
forceStream: false,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export type AdaptedRunnerSetup = {
|
|
169
|
+
system: string;
|
|
170
|
+
systemComponents: string[];
|
|
171
|
+
additionalSystemMessages: Array<{
|
|
172
|
+
role: 'system' | 'user';
|
|
173
|
+
content: string;
|
|
174
|
+
}>;
|
|
175
|
+
maxOutputTokens: number | undefined;
|
|
176
|
+
providerOptions: SharedV3ProviderOptions;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Adapt the main chat runner's system prompt placement, maxOutputTokens,
|
|
181
|
+
* and providerOptions based on the OAuth context.
|
|
182
|
+
*
|
|
183
|
+
* Unlike `adaptSimpleCall` (which builds the full message), this only
|
|
184
|
+
* decides WHERE the already-composed system prompt goes:
|
|
185
|
+
*
|
|
186
|
+
* - **OpenAI OAuth (Codex)**: system='', composed prompt sent as a user
|
|
187
|
+
* message in additionalSystemMessages, providerOptions with store=false
|
|
188
|
+
* + instructions, maxOutputTokens stripped.
|
|
189
|
+
*
|
|
190
|
+
* - **Anthropic OAuth**: spoof prompt as system, composed prompt sent as
|
|
191
|
+
* an additional system message. Normal maxOutputTokens.
|
|
192
|
+
*
|
|
193
|
+
* - **API key (default)**: composed prompt IS the system prompt directly.
|
|
194
|
+
* No additional messages needed.
|
|
195
|
+
*
|
|
196
|
+
* ```ts
|
|
197
|
+
* const composed = await composeSystemPrompt({ ... });
|
|
198
|
+
* const adapted = adaptRunnerCall(oauth, composed, { provider, rawMaxOutputTokens });
|
|
199
|
+
* // adapted.system, adapted.additionalSystemMessages, adapted.providerOptions ready to use
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
export function adaptRunnerCall(
|
|
203
|
+
ctx: OAuthContext,
|
|
204
|
+
composed: { prompt: string; components: string[] },
|
|
205
|
+
opts: {
|
|
206
|
+
provider: string;
|
|
207
|
+
rawMaxOutputTokens: number | undefined;
|
|
208
|
+
},
|
|
209
|
+
): AdaptedRunnerSetup {
|
|
210
|
+
if (ctx.spoofPrompt) {
|
|
211
|
+
return {
|
|
212
|
+
system: ctx.spoofPrompt,
|
|
213
|
+
systemComponents: [`spoof:${opts.provider || 'unknown'}`],
|
|
214
|
+
additionalSystemMessages: [{ role: 'system', content: composed.prompt }],
|
|
215
|
+
maxOutputTokens: opts.rawMaxOutputTokens,
|
|
216
|
+
providerOptions: {},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (ctx.isOpenAIOAuth) {
|
|
221
|
+
return {
|
|
222
|
+
system: '',
|
|
223
|
+
systemComponents: composed.components,
|
|
224
|
+
additionalSystemMessages: [{ role: 'user', content: composed.prompt }],
|
|
225
|
+
maxOutputTokens: undefined,
|
|
226
|
+
providerOptions: buildCodexProviderOptions(composed.prompt),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
system: composed.prompt,
|
|
232
|
+
systemComponents: composed.components,
|
|
233
|
+
additionalSystemMessages: [],
|
|
234
|
+
maxOutputTokens: opts.rawMaxOutputTokens,
|
|
235
|
+
providerOptions: {},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { OttoConfig } from '@ottocode/sdk';
|
|
2
|
+
import { getAuth, createOpenAIOAuthModel } from '@ottocode/sdk';
|
|
3
|
+
import { openai, createOpenAI } from '@ai-sdk/openai';
|
|
4
|
+
|
|
5
|
+
export async function resolveOpenAIModel(model: string, cfg: OttoConfig) {
|
|
6
|
+
const auth = await getAuth('openai', cfg.projectRoot);
|
|
7
|
+
if (auth?.type === 'oauth') {
|
|
8
|
+
return createOpenAIOAuthModel(model, {
|
|
9
|
+
oauth: auth,
|
|
10
|
+
projectRoot: cfg.projectRoot,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
if (auth?.type === 'api' && auth.key) {
|
|
14
|
+
const instance = createOpenAI({ apiKey: auth.key });
|
|
15
|
+
return instance(model);
|
|
16
|
+
}
|
|
17
|
+
return openai(model);
|
|
18
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { OttoConfig } from '@ottocode/sdk';
|
|
2
|
+
import { createOpencodeModel } from '@ottocode/sdk';
|
|
3
|
+
|
|
4
|
+
export function resolveOpencodeModel(model: string, _cfg: OttoConfig) {
|
|
5
|
+
const apiKey = process.env.OPENCODE_API_KEY;
|
|
6
|
+
return createOpencodeModel(model, { apiKey });
|
|
7
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { OttoConfig } from '@ottocode/sdk';
|
|
2
|
+
import {
|
|
3
|
+
catalog,
|
|
4
|
+
type ProviderId,
|
|
5
|
+
isProviderAuthorized,
|
|
6
|
+
providerIds,
|
|
7
|
+
defaultModelFor,
|
|
8
|
+
hasModel,
|
|
9
|
+
} from '@ottocode/sdk';
|
|
10
|
+
|
|
11
|
+
const FALLBACK_ORDER: ProviderId[] = [
|
|
12
|
+
'anthropic',
|
|
13
|
+
'openai',
|
|
14
|
+
'google',
|
|
15
|
+
'opencode',
|
|
16
|
+
'openrouter',
|
|
17
|
+
'setu',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
type SelectionInput = {
|
|
21
|
+
cfg: OttoConfig;
|
|
22
|
+
agentProviderDefault: ProviderId;
|
|
23
|
+
agentModelDefault: string;
|
|
24
|
+
explicitProvider?: ProviderId;
|
|
25
|
+
explicitModel?: string;
|
|
26
|
+
skipAuth?: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type ProviderSelection = {
|
|
30
|
+
provider: ProviderId;
|
|
31
|
+
model: string;
|
|
32
|
+
providerOverride?: ProviderId;
|
|
33
|
+
modelOverride?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export async function selectProviderAndModel(
|
|
37
|
+
input: SelectionInput,
|
|
38
|
+
): Promise<ProviderSelection> {
|
|
39
|
+
const {
|
|
40
|
+
cfg,
|
|
41
|
+
agentProviderDefault,
|
|
42
|
+
agentModelDefault,
|
|
43
|
+
explicitProvider,
|
|
44
|
+
explicitModel,
|
|
45
|
+
skipAuth,
|
|
46
|
+
} = input;
|
|
47
|
+
|
|
48
|
+
const provider = skipAuth
|
|
49
|
+
? (explicitProvider ?? agentProviderDefault)
|
|
50
|
+
: await pickAuthorizedProvider({
|
|
51
|
+
cfg,
|
|
52
|
+
candidate: explicitProvider ?? agentProviderDefault,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!provider) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
'No authorized providers found. Run `otto auth login` to configure at least one provider.',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const model = resolveModelForProvider({
|
|
62
|
+
provider,
|
|
63
|
+
explicitModel,
|
|
64
|
+
agentModelDefault,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const providerOverride =
|
|
68
|
+
explicitProvider ??
|
|
69
|
+
(provider !== agentProviderDefault ? provider : undefined);
|
|
70
|
+
const modelOverride =
|
|
71
|
+
explicitModel ?? (model !== agentModelDefault ? model : undefined);
|
|
72
|
+
|
|
73
|
+
return { provider, model, providerOverride, modelOverride };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function pickAuthorizedProvider(args: {
|
|
77
|
+
cfg: OttoConfig;
|
|
78
|
+
candidate: ProviderId;
|
|
79
|
+
}): Promise<ProviderId | undefined> {
|
|
80
|
+
const { cfg, candidate } = args;
|
|
81
|
+
const candidates = uniqueProviders([
|
|
82
|
+
candidate,
|
|
83
|
+
...FALLBACK_ORDER,
|
|
84
|
+
...providerIds,
|
|
85
|
+
]);
|
|
86
|
+
for (const provider of candidates) {
|
|
87
|
+
const ok = await isProviderAuthorized(cfg, provider);
|
|
88
|
+
if (ok) return provider;
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function uniqueProviders(list: ProviderId[]): ProviderId[] {
|
|
94
|
+
const seen = new Set<ProviderId>();
|
|
95
|
+
const ordered: ProviderId[] = [];
|
|
96
|
+
for (const provider of list) {
|
|
97
|
+
if (!providerIds.includes(provider)) continue;
|
|
98
|
+
if (seen.has(provider)) continue;
|
|
99
|
+
seen.add(provider);
|
|
100
|
+
ordered.push(provider);
|
|
101
|
+
}
|
|
102
|
+
return ordered;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resolveModelForProvider(args: {
|
|
106
|
+
provider: ProviderId;
|
|
107
|
+
explicitModel?: string;
|
|
108
|
+
agentModelDefault: string;
|
|
109
|
+
}): string {
|
|
110
|
+
const { provider, explicitModel, agentModelDefault } = args;
|
|
111
|
+
if (explicitModel && hasModel(provider, explicitModel)) return explicitModel;
|
|
112
|
+
if (hasModel(provider, agentModelDefault)) return agentModelDefault;
|
|
113
|
+
return (
|
|
114
|
+
defaultModelFor(provider) ??
|
|
115
|
+
catalog[provider]?.models?.[0]?.id ??
|
|
116
|
+
agentModelDefault
|
|
117
|
+
);
|
|
118
|
+
}
|