@ottocode/server 0.1.244 → 0.1.246
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 +4 -3
- package/src/events/types.ts +9 -9
- package/src/index.ts +9 -4
- package/src/openapi/paths/auth.ts +11 -11
- package/src/openapi/paths/config.ts +118 -2
- package/src/openapi/paths/{setu.ts → ottorouter.ts} +31 -31
- package/src/openapi/paths/skills.ts +122 -0
- package/src/openapi/schemas.ts +35 -3
- package/src/openapi/spec.ts +3 -3
- package/src/routes/auth.ts +40 -46
- package/src/routes/branch.ts +3 -2
- package/src/routes/config/defaults.ts +10 -3
- package/src/routes/config/main.ts +3 -0
- package/src/routes/config/models.ts +84 -14
- package/src/routes/config/providers.ts +137 -4
- package/src/routes/config/utils.ts +72 -2
- package/src/routes/doctor.ts +15 -27
- package/src/routes/git/commit.ts +16 -5
- package/src/routes/{setu.ts → ottorouter.ts} +52 -49
- package/src/routes/research.ts +3 -3
- package/src/routes/session-messages.ts +14 -8
- package/src/routes/sessions.ts +12 -18
- package/src/routes/skills.ts +140 -59
- package/src/runtime/agent/registry.ts +5 -2
- package/src/runtime/agent/runner-setup.ts +123 -38
- package/src/runtime/agent/runner.ts +140 -4
- package/src/runtime/ask/service.ts +14 -11
- package/src/runtime/message/history-builder.ts +22 -6
- package/src/runtime/message/service.ts +7 -1
- package/src/runtime/prompt/builder.ts +12 -0
- package/src/runtime/prompt/capabilities.ts +200 -0
- package/src/runtime/provider/index.ts +106 -5
- package/src/runtime/provider/{setu.ts → ottorouter.ts} +22 -22
- package/src/runtime/provider/reasoning.ts +73 -17
- package/src/runtime/provider/selection.ts +17 -15
- package/src/runtime/session/db-operations.ts +1 -1
- package/src/runtime/session/manager.ts +1 -1
- package/src/runtime/session/queue.ts +7 -2
- package/src/runtime/stream/error-handler.ts +3 -3
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import type { OttoConfig, ProviderId } from '@ottocode/sdk';
|
|
2
|
+
import {
|
|
3
|
+
getConfiguredProviderApiKey,
|
|
4
|
+
getProviderDefinition,
|
|
5
|
+
isBuiltInProviderId,
|
|
6
|
+
normalizeOllamaBaseURL,
|
|
7
|
+
} from '@ottocode/sdk';
|
|
8
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
9
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
10
|
+
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
11
|
+
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
12
|
+
import { createOllama } from 'ai-sdk-ollama';
|
|
13
|
+
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
|
2
14
|
import { getAnthropicInstance } from './anthropic.ts';
|
|
3
15
|
import { resolveOpenAIModel } from './openai.ts';
|
|
4
16
|
import { resolveGoogleModel } from './google.ts';
|
|
5
17
|
import { resolveOpenRouterModel } from './openrouter.ts';
|
|
6
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
resolveOttoRouterModel,
|
|
20
|
+
type ResolveOttoRouterModelOptions,
|
|
21
|
+
} from './ottorouter.ts';
|
|
7
22
|
import { getZaiInstance, getZaiCodingInstance } from './zai.ts';
|
|
8
23
|
import { resolveOpencodeModel } from './opencode.ts';
|
|
9
24
|
import { getMoonshotInstance } from './moonshot.ts';
|
|
@@ -20,8 +35,9 @@ export async function resolveModel(
|
|
|
20
35
|
systemPrompt?: string;
|
|
21
36
|
sessionId?: string;
|
|
22
37
|
messageId?: string;
|
|
23
|
-
|
|
24
|
-
|
|
38
|
+
reasoningText?: boolean;
|
|
39
|
+
topupApprovalMode?: ResolveOttoRouterModelOptions['topupApprovalMode'];
|
|
40
|
+
autoPayThresholdUsd?: ResolveOttoRouterModelOptions['autoPayThresholdUsd'];
|
|
25
41
|
},
|
|
26
42
|
) {
|
|
27
43
|
if (provider === 'openai') {
|
|
@@ -34,6 +50,13 @@ export async function resolveModel(
|
|
|
34
50
|
if (provider === 'google') {
|
|
35
51
|
return resolveGoogleModel(model, cfg);
|
|
36
52
|
}
|
|
53
|
+
if (provider === 'ollama-cloud') {
|
|
54
|
+
const definition = getProviderDefinition(cfg, provider);
|
|
55
|
+
if (!definition) {
|
|
56
|
+
throw new Error(`Unsupported provider: ${provider}`);
|
|
57
|
+
}
|
|
58
|
+
return resolveCustomConfiguredModel(definition, cfg, model, options);
|
|
59
|
+
}
|
|
37
60
|
if (provider === 'openrouter') {
|
|
38
61
|
return resolveOpenRouterModel(model);
|
|
39
62
|
}
|
|
@@ -43,8 +66,8 @@ export async function resolveModel(
|
|
|
43
66
|
if (provider === 'copilot') {
|
|
44
67
|
return resolveCopilotModel(model, cfg);
|
|
45
68
|
}
|
|
46
|
-
if (provider === '
|
|
47
|
-
return await
|
|
69
|
+
if (provider === 'ottorouter') {
|
|
70
|
+
return await resolveOttoRouterModel(model, options?.sessionId, {
|
|
48
71
|
messageId: options?.messageId,
|
|
49
72
|
topupApprovalMode: options?.topupApprovalMode,
|
|
50
73
|
autoPayThresholdUsd: options?.autoPayThresholdUsd,
|
|
@@ -62,5 +85,83 @@ export async function resolveModel(
|
|
|
62
85
|
if (provider === 'minimax') {
|
|
63
86
|
return getMinimaxInstance(cfg, model);
|
|
64
87
|
}
|
|
88
|
+
|
|
89
|
+
const definition = getProviderDefinition(cfg, provider);
|
|
90
|
+
if (definition && !isBuiltInProviderId(provider)) {
|
|
91
|
+
return resolveCustomConfiguredModel(definition, cfg, model, options);
|
|
92
|
+
}
|
|
65
93
|
throw new Error(`Unsupported provider: ${provider}`);
|
|
66
94
|
}
|
|
95
|
+
|
|
96
|
+
function needsResponsesApi(model: string): boolean {
|
|
97
|
+
const lower = model.toLowerCase();
|
|
98
|
+
return (
|
|
99
|
+
lower.includes('gpt-5') ||
|
|
100
|
+
lower.startsWith('o1') ||
|
|
101
|
+
lower.startsWith('o3') ||
|
|
102
|
+
lower.startsWith('o4') ||
|
|
103
|
+
lower.includes('codex-mini')
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function resolveCustomConfiguredModel(
|
|
108
|
+
definition: NonNullable<ReturnType<typeof getProviderDefinition>>,
|
|
109
|
+
cfg: OttoConfig,
|
|
110
|
+
model: string,
|
|
111
|
+
options?: {
|
|
112
|
+
reasoningText?: boolean;
|
|
113
|
+
},
|
|
114
|
+
) {
|
|
115
|
+
const apiKey = getConfiguredProviderApiKey(cfg, definition.id) || '';
|
|
116
|
+
const baseURL =
|
|
117
|
+
definition.baseURL ||
|
|
118
|
+
(definition.id === 'ollama-cloud' ? 'https://ollama.com' : undefined);
|
|
119
|
+
|
|
120
|
+
if (!baseURL) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Custom provider ${definition.id} requires a baseURL in config.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (definition.compatibility === 'openai') {
|
|
127
|
+
const instance = createOpenAI({ apiKey, baseURL });
|
|
128
|
+
return needsResponsesApi(model)
|
|
129
|
+
? instance.responses(model)
|
|
130
|
+
: instance(model);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (definition.compatibility === 'anthropic') {
|
|
134
|
+
const instance = createAnthropic({ apiKey, baseURL });
|
|
135
|
+
return instance(model);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (definition.compatibility === 'google') {
|
|
139
|
+
const instance = createGoogleGenerativeAI({ apiKey, baseURL });
|
|
140
|
+
return instance(model);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (definition.compatibility === 'openrouter') {
|
|
144
|
+
const instance = createOpenRouter({ apiKey, baseURL });
|
|
145
|
+
return instance.chat(model);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (definition.compatibility === 'ollama') {
|
|
149
|
+
const headers = apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined;
|
|
150
|
+
const ollamaBaseURL = normalizeOllamaBaseURL(baseURL);
|
|
151
|
+
const instance = createOllama({
|
|
152
|
+
baseURL: ollamaBaseURL,
|
|
153
|
+
headers,
|
|
154
|
+
});
|
|
155
|
+
return instance(model, {
|
|
156
|
+
...(options?.reasoningText ? { think: true } : {}),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const headers = apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined;
|
|
161
|
+
const instance = createOpenAICompatible({
|
|
162
|
+
name: definition.label,
|
|
163
|
+
baseURL,
|
|
164
|
+
headers,
|
|
165
|
+
});
|
|
166
|
+
return instance(model);
|
|
167
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
type
|
|
2
|
+
createOttoRouter,
|
|
3
|
+
type OttoRouterPaymentCallbacks,
|
|
4
4
|
getAuth,
|
|
5
5
|
loadConfig,
|
|
6
6
|
} from '@ottocode/sdk';
|
|
@@ -14,19 +14,19 @@ import {
|
|
|
14
14
|
|
|
15
15
|
const MIN_TOPUP_USD = 5;
|
|
16
16
|
|
|
17
|
-
export interface
|
|
17
|
+
export interface ResolveOttoRouterModelOptions {
|
|
18
18
|
messageId?: string;
|
|
19
19
|
topupApprovalMode?: 'auto' | 'approval';
|
|
20
20
|
autoPayThresholdUsd?: number;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
async function
|
|
24
|
-
if (process.env.
|
|
25
|
-
return process.env.
|
|
23
|
+
async function getOttoRouterPrivateKey(): Promise<string> {
|
|
24
|
+
if (process.env.OTTOROUTER_PRIVATE_KEY) {
|
|
25
|
+
return process.env.OTTOROUTER_PRIVATE_KEY;
|
|
26
26
|
}
|
|
27
27
|
try {
|
|
28
28
|
const cfg = await loadConfig(process.cwd());
|
|
29
|
-
const auth = await getAuth('
|
|
29
|
+
const auth = await getAuth('ottorouter', cfg.projectRoot);
|
|
30
30
|
if (auth?.type === 'wallet' && auth.secret) {
|
|
31
31
|
return auth.secret;
|
|
32
32
|
}
|
|
@@ -34,58 +34,58 @@ async function getSetuPrivateKey(): Promise<string> {
|
|
|
34
34
|
return '';
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export async function
|
|
37
|
+
export async function resolveOttoRouterModel(
|
|
38
38
|
model: string,
|
|
39
39
|
sessionId?: string,
|
|
40
|
-
options:
|
|
40
|
+
options: ResolveOttoRouterModelOptions = {},
|
|
41
41
|
) {
|
|
42
|
-
const privateKey = await
|
|
42
|
+
const privateKey = await getOttoRouterPrivateKey();
|
|
43
43
|
if (!privateKey) {
|
|
44
44
|
throw new Error(
|
|
45
|
-
'
|
|
45
|
+
'OttoRouter provider requires OTTOROUTER_PRIVATE_KEY (base58 Solana secret).',
|
|
46
46
|
);
|
|
47
47
|
}
|
|
48
|
-
const baseURL = process.env.
|
|
49
|
-
const rpcURL = process.env.
|
|
48
|
+
const baseURL = process.env.OTTOROUTER_BASE_URL;
|
|
49
|
+
const rpcURL = process.env.OTTOROUTER_SOLANA_RPC_URL;
|
|
50
50
|
const {
|
|
51
51
|
messageId,
|
|
52
52
|
topupApprovalMode = 'approval',
|
|
53
53
|
autoPayThresholdUsd = MIN_TOPUP_USD,
|
|
54
54
|
} = options;
|
|
55
55
|
|
|
56
|
-
const callbacks:
|
|
56
|
+
const callbacks: OttoRouterPaymentCallbacks = sessionId
|
|
57
57
|
? {
|
|
58
58
|
onPaymentRequired: (amountUsd, currentBalance) => {
|
|
59
59
|
publish({
|
|
60
|
-
type: '
|
|
60
|
+
type: 'ottorouter.payment.required',
|
|
61
61
|
sessionId,
|
|
62
62
|
payload: { amountUsd, currentBalance },
|
|
63
63
|
});
|
|
64
64
|
},
|
|
65
65
|
onPaymentSigning: () => {
|
|
66
66
|
publish({
|
|
67
|
-
type: '
|
|
67
|
+
type: 'ottorouter.payment.signing',
|
|
68
68
|
sessionId,
|
|
69
69
|
payload: {},
|
|
70
70
|
});
|
|
71
71
|
},
|
|
72
72
|
onPaymentComplete: (data) => {
|
|
73
73
|
publish({
|
|
74
|
-
type: '
|
|
74
|
+
type: 'ottorouter.payment.complete',
|
|
75
75
|
sessionId,
|
|
76
76
|
payload: data,
|
|
77
77
|
});
|
|
78
78
|
},
|
|
79
79
|
onPaymentError: (error) => {
|
|
80
80
|
publish({
|
|
81
|
-
type: '
|
|
81
|
+
type: 'ottorouter.payment.error',
|
|
82
82
|
sessionId,
|
|
83
83
|
payload: { error },
|
|
84
84
|
});
|
|
85
85
|
},
|
|
86
86
|
onBalanceUpdate: (update) => {
|
|
87
87
|
publish({
|
|
88
|
-
type: '
|
|
88
|
+
type: 'ottorouter.balance.updated',
|
|
89
89
|
sessionId,
|
|
90
90
|
payload: update,
|
|
91
91
|
});
|
|
@@ -97,7 +97,7 @@ export async function resolveSetuModel(
|
|
|
97
97
|
);
|
|
98
98
|
|
|
99
99
|
publish({
|
|
100
|
-
type: '
|
|
100
|
+
type: 'ottorouter.topup.required',
|
|
101
101
|
sessionId,
|
|
102
102
|
payload: {
|
|
103
103
|
messageId,
|
|
@@ -118,7 +118,7 @@ export async function resolveSetuModel(
|
|
|
118
118
|
}
|
|
119
119
|
: {};
|
|
120
120
|
|
|
121
|
-
const
|
|
121
|
+
const ottorouter = createOttoRouter({
|
|
122
122
|
auth: { privateKey },
|
|
123
123
|
baseURL,
|
|
124
124
|
rpcURL,
|
|
@@ -130,5 +130,5 @@ export async function resolveSetuModel(
|
|
|
130
130
|
},
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
-
return
|
|
133
|
+
return ottorouter.model(model);
|
|
134
134
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
catalog,
|
|
3
|
+
getConfiguredProviderFamily,
|
|
4
|
+
getProviderDefinition,
|
|
3
5
|
getModelNpmBinding,
|
|
4
6
|
getUnderlyingProviderKey,
|
|
5
7
|
modelSupportsReasoning,
|
|
8
|
+
type OttoConfig,
|
|
6
9
|
type ProviderId,
|
|
7
10
|
type ReasoningLevel,
|
|
8
11
|
} from '@ottocode/sdk';
|
|
@@ -35,7 +38,6 @@ function toAnthropicEffort(
|
|
|
35
38
|
case 'max':
|
|
36
39
|
case 'xhigh':
|
|
37
40
|
return 'max';
|
|
38
|
-
case 'high':
|
|
39
41
|
default:
|
|
40
42
|
return 'high';
|
|
41
43
|
}
|
|
@@ -54,7 +56,6 @@ function toOpenAIEffort(
|
|
|
54
56
|
case 'max':
|
|
55
57
|
case 'xhigh':
|
|
56
58
|
return 'xhigh';
|
|
57
|
-
case 'high':
|
|
58
59
|
default:
|
|
59
60
|
return 'high';
|
|
60
61
|
}
|
|
@@ -70,9 +71,6 @@ function toGoogleThinkingLevel(
|
|
|
70
71
|
return 'low';
|
|
71
72
|
case 'medium':
|
|
72
73
|
return 'medium';
|
|
73
|
-
case 'max':
|
|
74
|
-
case 'xhigh':
|
|
75
|
-
case 'high':
|
|
76
74
|
default:
|
|
77
75
|
return 'high';
|
|
78
76
|
}
|
|
@@ -95,7 +93,6 @@ function toThinkingBudget(
|
|
|
95
93
|
case 'max':
|
|
96
94
|
case 'xhigh':
|
|
97
95
|
return Math.min(24000, cap);
|
|
98
|
-
case 'high':
|
|
99
96
|
default:
|
|
100
97
|
return Math.min(16000, cap);
|
|
101
98
|
}
|
|
@@ -114,11 +111,16 @@ function toCamelCaseKey(value: string): string {
|
|
|
114
111
|
.join('');
|
|
115
112
|
}
|
|
116
113
|
|
|
117
|
-
function getOpenAICompatibleProviderOptionKeys(
|
|
114
|
+
function getOpenAICompatibleProviderOptionKeys(
|
|
115
|
+
provider: ProviderId,
|
|
116
|
+
cfg?: OttoConfig,
|
|
117
|
+
): string[] {
|
|
118
|
+
const definition = cfg ? getProviderDefinition(cfg, provider) : undefined;
|
|
118
119
|
const entry = catalog[provider];
|
|
119
120
|
const keys = new Set<string>(['openaiCompatible', toCamelCaseKey(provider)]);
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
const label = definition?.label ?? entry?.label;
|
|
122
|
+
if (label) {
|
|
123
|
+
keys.add(toCamelCaseKey(label));
|
|
122
124
|
}
|
|
123
125
|
return Array.from(keys).filter(Boolean);
|
|
124
126
|
}
|
|
@@ -126,8 +128,9 @@ function getOpenAICompatibleProviderOptionKeys(provider: ProviderId): string[] {
|
|
|
126
128
|
function buildSharedProviderOptions(
|
|
127
129
|
provider: ProviderId,
|
|
128
130
|
options: Record<string, unknown>,
|
|
131
|
+
cfg?: OttoConfig,
|
|
129
132
|
): Record<string, unknown> {
|
|
130
|
-
const keys = getOpenAICompatibleProviderOptionKeys(provider);
|
|
133
|
+
const keys = getOpenAICompatibleProviderOptionKeys(provider, cfg);
|
|
131
134
|
return Object.fromEntries(keys.map((key) => [key, options]));
|
|
132
135
|
}
|
|
133
136
|
|
|
@@ -146,14 +149,28 @@ function usesAdaptiveAnthropicThinking(model: string): boolean {
|
|
|
146
149
|
function getReasoningProviderTarget(
|
|
147
150
|
provider: ProviderId,
|
|
148
151
|
model: string,
|
|
152
|
+
cfg?: OttoConfig,
|
|
149
153
|
):
|
|
150
154
|
| 'anthropic'
|
|
151
155
|
| 'openai'
|
|
152
156
|
| 'google'
|
|
157
|
+
| 'ollama'
|
|
153
158
|
| 'openai-compatible'
|
|
154
159
|
| 'openrouter'
|
|
155
160
|
| null {
|
|
161
|
+
const definition = cfg ? getProviderDefinition(cfg, provider) : undefined;
|
|
162
|
+
if (definition?.source === 'custom') {
|
|
163
|
+
if (definition.compatibility === 'anthropic') return 'anthropic';
|
|
164
|
+
if (definition.compatibility === 'openai') return 'openai';
|
|
165
|
+
if (definition.compatibility === 'google') return 'google';
|
|
166
|
+
if (definition.compatibility === 'ollama') return 'ollama';
|
|
167
|
+
if (definition.compatibility === 'openrouter') return 'openrouter';
|
|
168
|
+
return 'openai-compatible';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (provider === 'ottorouter') return 'openrouter';
|
|
156
172
|
if (provider === 'openrouter') return 'openrouter';
|
|
173
|
+
if (definition?.compatibility === 'ollama') return 'ollama';
|
|
157
174
|
if (
|
|
158
175
|
provider === 'moonshot' ||
|
|
159
176
|
provider === 'zai' ||
|
|
@@ -167,6 +184,7 @@ function getReasoningProviderTarget(
|
|
|
167
184
|
if (npmBinding === '@ai-sdk/anthropic') return 'anthropic';
|
|
168
185
|
if (npmBinding === '@ai-sdk/openai') return 'openai';
|
|
169
186
|
if (npmBinding === '@ai-sdk/google') return 'google';
|
|
187
|
+
if (npmBinding === 'ai-sdk-ollama') return 'ollama';
|
|
170
188
|
if (npmBinding === '@ai-sdk/openai-compatible') return 'openai-compatible';
|
|
171
189
|
if (npmBinding === '@openrouter/ai-sdk-provider') return 'openrouter';
|
|
172
190
|
|
|
@@ -175,19 +193,41 @@ function getReasoningProviderTarget(
|
|
|
175
193
|
if (underlyingProvider === 'openai') return 'openai';
|
|
176
194
|
if (underlyingProvider === 'google') return 'google';
|
|
177
195
|
if (underlyingProvider === 'openai-compatible') return 'openai-compatible';
|
|
196
|
+
|
|
197
|
+
const family = cfg ? getConfiguredProviderFamily(cfg, provider, model) : null;
|
|
198
|
+
if (family === 'anthropic') return 'anthropic';
|
|
199
|
+
if (family === 'openai') return 'openai';
|
|
200
|
+
if (family === 'google') return 'google';
|
|
201
|
+
if (family === 'openai-compatible') return 'openai-compatible';
|
|
178
202
|
return null;
|
|
179
203
|
}
|
|
180
204
|
|
|
181
205
|
export function buildReasoningConfig(args: {
|
|
206
|
+
cfg?: OttoConfig;
|
|
182
207
|
provider: ProviderId;
|
|
183
208
|
model: string;
|
|
184
209
|
reasoningText?: boolean;
|
|
185
210
|
reasoningLevel?: ReasoningLevel;
|
|
186
211
|
maxOutputTokens: number | undefined;
|
|
187
212
|
}): ReasoningConfigResult {
|
|
188
|
-
const {
|
|
189
|
-
|
|
190
|
-
|
|
213
|
+
const {
|
|
214
|
+
cfg,
|
|
215
|
+
provider,
|
|
216
|
+
model,
|
|
217
|
+
reasoningText,
|
|
218
|
+
reasoningLevel,
|
|
219
|
+
maxOutputTokens,
|
|
220
|
+
} = args;
|
|
221
|
+
const definition = cfg ? getProviderDefinition(cfg, provider) : undefined;
|
|
222
|
+
const supportsReasoning =
|
|
223
|
+
definition?.compatibility === 'ollama'
|
|
224
|
+
? true
|
|
225
|
+
: definition?.source === 'custom'
|
|
226
|
+
? true
|
|
227
|
+
: provider === 'ottorouter'
|
|
228
|
+
? true
|
|
229
|
+
: modelSupportsReasoning(provider, model);
|
|
230
|
+
if (!reasoningText || !supportsReasoning) {
|
|
191
231
|
return {
|
|
192
232
|
providerOptions: {},
|
|
193
233
|
effectiveMaxOutputTokens: maxOutputTokens,
|
|
@@ -195,7 +235,7 @@ export function buildReasoningConfig(args: {
|
|
|
195
235
|
};
|
|
196
236
|
}
|
|
197
237
|
|
|
198
|
-
const reasoningTarget = getReasoningProviderTarget(provider, model);
|
|
238
|
+
const reasoningTarget = getReasoningProviderTarget(provider, model, cfg);
|
|
199
239
|
if (reasoningTarget === 'anthropic') {
|
|
200
240
|
if (usesAdaptiveAnthropicThinking(model)) {
|
|
201
241
|
return {
|
|
@@ -263,6 +303,18 @@ export function buildReasoningConfig(args: {
|
|
|
263
303
|
};
|
|
264
304
|
}
|
|
265
305
|
|
|
306
|
+
if (reasoningTarget === 'ollama') {
|
|
307
|
+
return {
|
|
308
|
+
providerOptions: {
|
|
309
|
+
ollama: {
|
|
310
|
+
think: true,
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
effectiveMaxOutputTokens: maxOutputTokens,
|
|
314
|
+
enabled: true,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
266
318
|
if (reasoningTarget === 'openrouter') {
|
|
267
319
|
return {
|
|
268
320
|
providerOptions: {
|
|
@@ -277,9 +329,13 @@ export function buildReasoningConfig(args: {
|
|
|
277
329
|
|
|
278
330
|
if (reasoningTarget === 'openai-compatible') {
|
|
279
331
|
return {
|
|
280
|
-
providerOptions: buildSharedProviderOptions(
|
|
281
|
-
|
|
282
|
-
|
|
332
|
+
providerOptions: buildSharedProviderOptions(
|
|
333
|
+
provider,
|
|
334
|
+
{
|
|
335
|
+
reasoningEffort: normalizeReasoningLevel(reasoningLevel),
|
|
336
|
+
},
|
|
337
|
+
cfg,
|
|
338
|
+
),
|
|
283
339
|
effectiveMaxOutputTokens: maxOutputTokens,
|
|
284
340
|
enabled: true,
|
|
285
341
|
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { OttoConfig } from '@ottocode/sdk';
|
|
2
2
|
import {
|
|
3
|
-
catalog,
|
|
4
3
|
type ProviderId,
|
|
5
4
|
isProviderAuthorized,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
getConfiguredProviderIds,
|
|
6
|
+
getConfiguredProviderDefaultModel,
|
|
7
|
+
hasConfiguredModel,
|
|
8
|
+
hasConfiguredProvider,
|
|
9
9
|
} from '@ottocode/sdk';
|
|
10
10
|
|
|
11
11
|
const FALLBACK_ORDER: ProviderId[] = [
|
|
@@ -14,7 +14,7 @@ const FALLBACK_ORDER: ProviderId[] = [
|
|
|
14
14
|
'google',
|
|
15
15
|
'opencode',
|
|
16
16
|
'openrouter',
|
|
17
|
-
'
|
|
17
|
+
'ottorouter',
|
|
18
18
|
];
|
|
19
19
|
|
|
20
20
|
type SelectionInput = {
|
|
@@ -59,6 +59,7 @@ export async function selectProviderAndModel(
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
const model = resolveModelForProvider({
|
|
62
|
+
cfg,
|
|
62
63
|
provider,
|
|
63
64
|
explicitModel,
|
|
64
65
|
agentModelDefault,
|
|
@@ -81,9 +82,10 @@ async function pickAuthorizedProvider(args: {
|
|
|
81
82
|
const candidates = uniqueProviders([
|
|
82
83
|
candidate,
|
|
83
84
|
...FALLBACK_ORDER,
|
|
84
|
-
...
|
|
85
|
+
...getConfiguredProviderIds(cfg),
|
|
85
86
|
]);
|
|
86
87
|
for (const provider of candidates) {
|
|
88
|
+
if (!hasConfiguredProvider(cfg, provider)) continue;
|
|
87
89
|
const ok = await isProviderAuthorized(cfg, provider);
|
|
88
90
|
if (ok) return provider;
|
|
89
91
|
}
|
|
@@ -94,7 +96,6 @@ function uniqueProviders(list: ProviderId[]): ProviderId[] {
|
|
|
94
96
|
const seen = new Set<ProviderId>();
|
|
95
97
|
const ordered: ProviderId[] = [];
|
|
96
98
|
for (const provider of list) {
|
|
97
|
-
if (!providerIds.includes(provider)) continue;
|
|
98
99
|
if (seen.has(provider)) continue;
|
|
99
100
|
seen.add(provider);
|
|
100
101
|
ordered.push(provider);
|
|
@@ -103,16 +104,17 @@ function uniqueProviders(list: ProviderId[]): ProviderId[] {
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
function resolveModelForProvider(args: {
|
|
107
|
+
cfg: OttoConfig;
|
|
106
108
|
provider: ProviderId;
|
|
107
109
|
explicitModel?: string;
|
|
108
110
|
agentModelDefault: string;
|
|
109
111
|
}): string {
|
|
110
|
-
const { provider, explicitModel, agentModelDefault } = args;
|
|
111
|
-
if (explicitModel &&
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
);
|
|
112
|
+
const { cfg, provider, explicitModel, agentModelDefault } = args;
|
|
113
|
+
if (explicitModel && hasConfiguredModel(cfg, provider, explicitModel)) {
|
|
114
|
+
return explicitModel;
|
|
115
|
+
}
|
|
116
|
+
if (hasConfiguredModel(cfg, provider, agentModelDefault)) {
|
|
117
|
+
return agentModelDefault;
|
|
118
|
+
}
|
|
119
|
+
return getConfiguredProviderDefaultModel(cfg, provider) ?? agentModelDefault;
|
|
118
120
|
}
|
|
@@ -29,7 +29,7 @@ export async function createSession({
|
|
|
29
29
|
model,
|
|
30
30
|
title,
|
|
31
31
|
}: CreateSessionInput): Promise<SessionRow> {
|
|
32
|
-
validateProviderModel(provider, model);
|
|
32
|
+
validateProviderModel(provider, model, cfg);
|
|
33
33
|
const authorized = await isProviderAuthorized(cfg, provider);
|
|
34
34
|
if (!authorized) {
|
|
35
35
|
throw new Error(
|
|
@@ -10,6 +10,7 @@ export type RunOpts = {
|
|
|
10
10
|
provider: ProviderName;
|
|
11
11
|
model: string;
|
|
12
12
|
projectRoot: string;
|
|
13
|
+
queuedAt?: number;
|
|
13
14
|
oneShot?: boolean;
|
|
14
15
|
userContext?: string;
|
|
15
16
|
estimatedInputTokens?: number;
|
|
@@ -74,7 +75,7 @@ function publishQueueState(sessionId: string) {
|
|
|
74
75
|
* Creates an abort controller per message.
|
|
75
76
|
*/
|
|
76
77
|
export function enqueueAssistantRun(
|
|
77
|
-
opts: Omit<RunOpts, 'abortSignal'>,
|
|
78
|
+
opts: Omit<RunOpts, 'abortSignal' | 'queuedAt'>,
|
|
78
79
|
processQueueFn: (sessionId: string) => Promise<void>,
|
|
79
80
|
) {
|
|
80
81
|
const abortController = new AbortController();
|
|
@@ -85,7 +86,11 @@ export function enqueueAssistantRun(
|
|
|
85
86
|
running: false,
|
|
86
87
|
currentMessageId: null,
|
|
87
88
|
};
|
|
88
|
-
state.queue.push({
|
|
89
|
+
state.queue.push({
|
|
90
|
+
...opts,
|
|
91
|
+
queuedAt: globalThis.performance?.now?.() ?? Date.now(),
|
|
92
|
+
abortSignal: abortController.signal,
|
|
93
|
+
});
|
|
89
94
|
runners.set(opts.sessionId, state);
|
|
90
95
|
|
|
91
96
|
publishQueueState(opts.sessionId);
|
|
@@ -28,7 +28,7 @@ export function createErrorHandler(
|
|
|
28
28
|
| undefined;
|
|
29
29
|
const causeError = errObj?.cause as Record<string, unknown> | undefined;
|
|
30
30
|
|
|
31
|
-
// Check for
|
|
31
|
+
// Check for OTTOROUTER_FIAT_SELECTED code specifically (not string matching)
|
|
32
32
|
const errorCode =
|
|
33
33
|
(errObj?.code as string) ??
|
|
34
34
|
((errObj?.error as Record<string, unknown>)?.code as string) ??
|
|
@@ -72,7 +72,7 @@ export function createErrorHandler(
|
|
|
72
72
|
(causeError?.message as string) ??
|
|
73
73
|
'';
|
|
74
74
|
|
|
75
|
-
const isFiatSelected = errorCode === '
|
|
75
|
+
const isFiatSelected = errorCode === 'OTTOROUTER_FIAT_SELECTED';
|
|
76
76
|
|
|
77
77
|
// Handle fiat payment selected - this is not an error, just a signal to pause
|
|
78
78
|
if (isFiatSelected) {
|
|
@@ -140,7 +140,7 @@ export function createErrorHandler(
|
|
|
140
140
|
|
|
141
141
|
// Emit a special event so UI knows to show topup modal
|
|
142
142
|
publish({
|
|
143
|
-
type: '
|
|
143
|
+
type: 'ottorouter.fiat.checkout_created',
|
|
144
144
|
sessionId: opts.sessionId,
|
|
145
145
|
payload: {
|
|
146
146
|
messageId: opts.assistantMessageId,
|