@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.
Files changed (39) hide show
  1. package/package.json +4 -3
  2. package/src/events/types.ts +9 -9
  3. package/src/index.ts +9 -4
  4. package/src/openapi/paths/auth.ts +11 -11
  5. package/src/openapi/paths/config.ts +118 -2
  6. package/src/openapi/paths/{setu.ts → ottorouter.ts} +31 -31
  7. package/src/openapi/paths/skills.ts +122 -0
  8. package/src/openapi/schemas.ts +35 -3
  9. package/src/openapi/spec.ts +3 -3
  10. package/src/routes/auth.ts +40 -46
  11. package/src/routes/branch.ts +3 -2
  12. package/src/routes/config/defaults.ts +10 -3
  13. package/src/routes/config/main.ts +3 -0
  14. package/src/routes/config/models.ts +84 -14
  15. package/src/routes/config/providers.ts +137 -4
  16. package/src/routes/config/utils.ts +72 -2
  17. package/src/routes/doctor.ts +15 -27
  18. package/src/routes/git/commit.ts +16 -5
  19. package/src/routes/{setu.ts → ottorouter.ts} +52 -49
  20. package/src/routes/research.ts +3 -3
  21. package/src/routes/session-messages.ts +14 -8
  22. package/src/routes/sessions.ts +12 -18
  23. package/src/routes/skills.ts +140 -59
  24. package/src/runtime/agent/registry.ts +5 -2
  25. package/src/runtime/agent/runner-setup.ts +123 -38
  26. package/src/runtime/agent/runner.ts +140 -4
  27. package/src/runtime/ask/service.ts +14 -11
  28. package/src/runtime/message/history-builder.ts +22 -6
  29. package/src/runtime/message/service.ts +7 -1
  30. package/src/runtime/prompt/builder.ts +12 -0
  31. package/src/runtime/prompt/capabilities.ts +200 -0
  32. package/src/runtime/provider/index.ts +106 -5
  33. package/src/runtime/provider/{setu.ts → ottorouter.ts} +22 -22
  34. package/src/runtime/provider/reasoning.ts +73 -17
  35. package/src/runtime/provider/selection.ts +17 -15
  36. package/src/runtime/session/db-operations.ts +1 -1
  37. package/src/runtime/session/manager.ts +1 -1
  38. package/src/runtime/session/queue.ts +7 -2
  39. 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 { resolveSetuModel, type ResolveSetuModelOptions } from './setu.ts';
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
- topupApprovalMode?: ResolveSetuModelOptions['topupApprovalMode'];
24
- autoPayThresholdUsd?: ResolveSetuModelOptions['autoPayThresholdUsd'];
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 === 'setu') {
47
- return await resolveSetuModel(model, options?.sessionId, {
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
- createSetu,
3
- type SetuPaymentCallbacks,
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 ResolveSetuModelOptions {
17
+ export interface ResolveOttoRouterModelOptions {
18
18
  messageId?: string;
19
19
  topupApprovalMode?: 'auto' | 'approval';
20
20
  autoPayThresholdUsd?: number;
21
21
  }
22
22
 
23
- async function getSetuPrivateKey(): Promise<string> {
24
- if (process.env.SETU_PRIVATE_KEY) {
25
- return process.env.SETU_PRIVATE_KEY;
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('setu', cfg.projectRoot);
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 resolveSetuModel(
37
+ export async function resolveOttoRouterModel(
38
38
  model: string,
39
39
  sessionId?: string,
40
- options: ResolveSetuModelOptions = {},
40
+ options: ResolveOttoRouterModelOptions = {},
41
41
  ) {
42
- const privateKey = await getSetuPrivateKey();
42
+ const privateKey = await getOttoRouterPrivateKey();
43
43
  if (!privateKey) {
44
44
  throw new Error(
45
- 'Setu provider requires SETU_PRIVATE_KEY (base58 Solana secret).',
45
+ 'OttoRouter provider requires OTTOROUTER_PRIVATE_KEY (base58 Solana secret).',
46
46
  );
47
47
  }
48
- const baseURL = process.env.SETU_BASE_URL;
49
- const rpcURL = process.env.SETU_SOLANA_RPC_URL;
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: SetuPaymentCallbacks = sessionId
56
+ const callbacks: OttoRouterPaymentCallbacks = sessionId
57
57
  ? {
58
58
  onPaymentRequired: (amountUsd, currentBalance) => {
59
59
  publish({
60
- type: 'setu.payment.required',
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: 'setu.payment.signing',
67
+ type: 'ottorouter.payment.signing',
68
68
  sessionId,
69
69
  payload: {},
70
70
  });
71
71
  },
72
72
  onPaymentComplete: (data) => {
73
73
  publish({
74
- type: 'setu.payment.complete',
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: 'setu.payment.error',
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: 'setu.balance.updated',
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: 'setu.topup.required',
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 setu = createSetu({
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 setu.model(model);
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(provider: ProviderId): string[] {
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
- if (entry?.label) {
121
- keys.add(toCamelCaseKey(entry.label));
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 { provider, model, reasoningText, reasoningLevel, maxOutputTokens } =
189
- args;
190
- if (!reasoningText || !modelSupportsReasoning(provider, model)) {
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(provider, {
281
- reasoningEffort: normalizeReasoningLevel(reasoningLevel),
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
- providerIds,
7
- defaultModelFor,
8
- hasModel,
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
- 'setu',
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
- ...providerIds,
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 && 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
- );
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
  }
@@ -71,7 +71,7 @@ export function resolveUsageProvider(
71
71
  model: string,
72
72
  ): ProviderId {
73
73
  if (
74
- provider !== 'setu' &&
74
+ provider !== 'ottorouter' &&
75
75
  provider !== 'openrouter' &&
76
76
  provider !== 'opencode'
77
77
  ) {
@@ -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({ ...opts, abortSignal: abortController.signal });
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 SETU_FIAT_SELECTED code specifically (not string matching)
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 === 'SETU_FIAT_SELECTED';
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: 'setu.fiat.checkout_created',
143
+ type: 'ottorouter.fiat.checkout_created',
144
144
  sessionId: opts.sessionId,
145
145
  payload: {
146
146
  messageId: opts.assistantMessageId,