@ottocode/sdk 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 +3 -2
  2. package/src/auth/src/index.ts +2 -2
  3. package/src/auth/src/wallet.ts +5 -5
  4. package/src/config/src/index.ts +7 -2
  5. package/src/config/src/manager.ts +106 -30
  6. package/src/core/src/index.ts +6 -1
  7. package/src/core/src/providers/resolver.ts +37 -9
  8. package/src/core/src/tools/builtin/bash.ts +2 -2
  9. package/src/core/src/tools/builtin/bash.txt +1 -1
  10. package/src/core/src/tools/builtin/fs/edit-shared.ts +1 -1
  11. package/src/core/src/tools/builtin/fs/edit.txt +2 -2
  12. package/src/core/src/tools/builtin/fs/write.txt +1 -1
  13. package/src/core/src/tools/loader.ts +133 -81
  14. package/src/core/src/utils/debug.ts +13 -0
  15. package/src/index.ts +47 -12
  16. package/src/prompts/src/agents/build.txt +3 -4
  17. package/src/prompts/src/providers/default.txt +1 -1
  18. package/src/prompts/src/providers/glm.txt +1 -1
  19. package/src/prompts/src/providers/google.txt +2 -2
  20. package/src/prompts/src/providers/moonshot.txt +1 -1
  21. package/src/prompts/src/providers/openai.txt +3 -3
  22. package/src/prompts/src/providers.ts +15 -0
  23. package/src/providers/src/authorization.ts +26 -1
  24. package/src/providers/src/catalog-manual.ts +41 -23
  25. package/src/providers/src/catalog-merged.ts +2 -2
  26. package/src/providers/src/catalog.ts +10284 -10283
  27. package/src/providers/src/env.ts +11 -6
  28. package/src/providers/src/index.ts +38 -12
  29. package/src/providers/src/ollama-discovery.ts +149 -0
  30. package/src/providers/src/{setu-client.ts → ottorouter-client.ts} +30 -30
  31. package/src/providers/src/pricing.ts +4 -1
  32. package/src/providers/src/registry.ts +258 -0
  33. package/src/providers/src/utils.ts +11 -4
  34. package/src/providers/src/validate.ts +63 -2
  35. package/src/skills/index.ts +3 -0
  36. package/src/skills/tool.ts +28 -36
  37. package/src/types/src/config.ts +34 -8
  38. package/src/types/src/index.ts +4 -0
  39. package/src/types/src/provider.ts +34 -4
@@ -1,15 +1,19 @@
1
1
  import { catalog } from './catalog-merged.ts';
2
2
  import type {
3
+ BuiltInProviderId,
3
4
  ProviderId,
4
5
  ModelInfo,
5
6
  ModelOwner,
6
7
  } from '../../types/src/index.ts';
7
8
  import { filterModelsForAuthType } from './oauth-models.ts';
8
9
 
9
- export const providerIds = Object.keys(catalog) as ProviderId[];
10
+ export const providerIds = Object.keys(catalog) as BuiltInProviderId[];
10
11
 
11
- export function isProviderId(value: unknown): value is ProviderId {
12
- return typeof value === 'string' && providerIds.includes(value as ProviderId);
12
+ export function isProviderId(value: unknown): value is BuiltInProviderId {
13
+ return (
14
+ typeof value === 'string' &&
15
+ providerIds.includes(value as BuiltInProviderId)
16
+ );
13
17
  }
14
18
 
15
19
  export function defaultModelFor(provider: ProviderId): string | undefined {
@@ -34,7 +38,7 @@ const PREFERRED_FAST_MODELS: Partial<Record<ProviderId, string[]>> = {
34
38
  google: ['gemini-2.0-flash-lite'],
35
39
  openrouter: ['anthropic/claude-3.5-haiku'],
36
40
  opencode: ['claude-3-5-haiku'],
37
- setu: ['kimi-k2-turbo-preview'],
41
+ ottorouter: ['kimi-k2-turbo-preview'],
38
42
  zai: ['glm-4.5-flash'],
39
43
  copilot: ['gpt-4.1-mini'],
40
44
  };
@@ -142,6 +146,7 @@ const DIRECT_PROVIDER_FAMILY: Partial<
142
146
  openai: 'openai',
143
147
  anthropic: 'anthropic',
144
148
  google: 'google',
149
+ 'ollama-cloud': 'openai-compatible',
145
150
  moonshot: 'moonshot',
146
151
  minimax: 'minimax',
147
152
  copilot: 'openai',
@@ -201,8 +206,10 @@ export function getUnderlyingProviderKey(
201
206
  if (npm === '@ai-sdk/anthropic') return 'anthropic';
202
207
  if (npm === '@ai-sdk/openai') return 'openai';
203
208
  if (npm === '@ai-sdk/google') return 'google';
209
+ if (npm === 'ai-sdk-ollama') return 'openai-compatible';
204
210
  if (npm === '@ai-sdk/openai-compatible') return 'openai-compatible';
205
211
  if (npm === '@openrouter/ai-sdk-provider') return 'openai-compatible';
212
+ if (provider === 'ottorouter') return 'openai-compatible';
206
213
  return null;
207
214
  }
208
215
 
@@ -1,5 +1,11 @@
1
1
  import { catalog } from './catalog-merged.ts';
2
- import type { ProviderId } from '../../types/src/index.ts';
2
+ import type { OttoConfig, ProviderId } from '../../types/src/index.ts';
3
+ import {
4
+ getProviderDefinition,
5
+ getConfiguredProviderModels,
6
+ hasConfiguredModel,
7
+ providerAllowsAnyModel,
8
+ } from './registry.ts';
3
9
 
4
10
  export type CapabilityRequest = {
5
11
  wantsToolCalls?: boolean;
@@ -9,8 +15,38 @@ export type CapabilityRequest = {
9
15
  export function validateProviderModel(
10
16
  provider: string,
11
17
  model: string,
18
+ cfgOrCap?: OttoConfig | CapabilityRequest,
12
19
  cap?: CapabilityRequest,
13
20
  ) {
21
+ const cfg = isOttoConfigLike(cfgOrCap) ? cfgOrCap : undefined;
22
+ const effectiveCap = isOttoConfigLike(cfgOrCap) ? cap : cfgOrCap;
23
+
24
+ if (cfg) {
25
+ const definition = getProviderDefinition(cfg, provider);
26
+ if (!definition) {
27
+ throw new Error(`Provider not supported: ${provider}`);
28
+ }
29
+ if (!providerAllowsAnyModel(cfg, provider)) {
30
+ if (!hasConfiguredModel(cfg, provider, model)) {
31
+ const list = getConfiguredProviderModels(cfg, provider)
32
+ .slice(0, 10)
33
+ .map((m) => m.id)
34
+ .join(', ');
35
+ throw new Error(
36
+ `Model not found for provider ${provider}: ${model}. Example models: ${list}${getConfiguredProviderModels(cfg, provider).length > 10 ? ', ...' : ''}`,
37
+ );
38
+ }
39
+ }
40
+
41
+ const entry = definition.models.find((m) => m.id === model);
42
+ if (entry) {
43
+ applyCapabilityValidation(model, entry, effectiveCap, {
44
+ strict: definition.source !== 'custom',
45
+ });
46
+ }
47
+ return;
48
+ }
49
+
14
50
  const p = provider as ProviderId;
15
51
  if (!catalog[p]) {
16
52
  throw new Error(`Provider not supported: ${provider}`);
@@ -25,10 +61,26 @@ export function validateProviderModel(
25
61
  `Model not found for provider ${provider}: ${model}. Example models: ${list}${catalog[p].models.length > 10 ? ', ...' : ''}`,
26
62
  );
27
63
  }
28
- if (cap?.wantsToolCalls && !entry.toolCall) {
64
+ applyCapabilityValidation(model, entry, effectiveCap, { strict: true });
65
+ }
66
+
67
+ function applyCapabilityValidation(
68
+ model: string,
69
+ entry: {
70
+ toolCall?: boolean;
71
+ modalities?: { input?: string[]; output?: string[] };
72
+ },
73
+ cap: CapabilityRequest | undefined,
74
+ options: { strict: boolean },
75
+ ) {
76
+ if (cap?.wantsToolCalls && entry.toolCall === false) {
29
77
  throw new Error(`Model ${model} does not support tool calls.`);
30
78
  }
79
+ if (!options.strict && cap?.wantsToolCalls && entry.toolCall === undefined) {
80
+ return;
81
+ }
31
82
  if (cap?.wantsVision) {
83
+ if (!options.strict && !entry.modalities) return;
32
84
  const inputs = entry.modalities?.input as string[] | undefined;
33
85
  const outputs = entry.modalities?.output as string[] | undefined;
34
86
  const ok =
@@ -37,3 +89,12 @@ export function validateProviderModel(
37
89
  throw new Error(`Model ${model} does not support vision input/output.`);
38
90
  }
39
91
  }
92
+
93
+ function isOttoConfigLike(value: unknown): value is OttoConfig {
94
+ return Boolean(
95
+ value &&
96
+ typeof value === 'object' &&
97
+ 'defaults' in value &&
98
+ 'providers' in value,
99
+ );
100
+ }
@@ -32,8 +32,11 @@ export {
32
32
  export {
33
33
  initializeSkills,
34
34
  getDiscoveredSkills,
35
+ setSkillSettings,
36
+ filterDiscoveredSkills,
35
37
  isSkillsInitialized,
36
38
  buildSkillTool,
39
+ summarizeDescription,
37
40
  rebuildSkillDescription,
38
41
  } from './tool.ts';
39
42
 
@@ -9,9 +9,11 @@ import {
9
9
  } from './loader.ts';
10
10
  import { scanContent } from './security.ts';
11
11
  import type { DiscoveredSkill, SkillResult } from './types.ts';
12
+ import type { SkillSettings } from '../types/src/config.ts';
12
13
 
13
14
  let cachedSkillList: DiscoveredSkill[] = [];
14
15
  let initializedForPath: string | null = null;
16
+ let cachedSkillSettings: SkillSettings | undefined;
15
17
 
16
18
  export async function initializeSkills(
17
19
  cwd: string,
@@ -27,6 +29,23 @@ export function getDiscoveredSkills(): DiscoveredSkill[] {
27
29
  return cachedSkillList;
28
30
  }
29
31
 
32
+ export function setSkillSettings(settings?: SkillSettings): void {
33
+ cachedSkillSettings = settings;
34
+ }
35
+
36
+ export function filterDiscoveredSkills(
37
+ skills: DiscoveredSkill[],
38
+ settings?: {
39
+ enabled?: boolean;
40
+ items?: Record<string, { enabled?: boolean }>;
41
+ },
42
+ ): DiscoveredSkill[] {
43
+ if (settings?.enabled === false) return [];
44
+ return skills.filter(
45
+ (skill) => settings?.items?.[skill.name]?.enabled !== false,
46
+ );
47
+ }
48
+
30
49
  export function isSkillsInitialized(forPath?: string): boolean {
31
50
  if (!initializedForPath) return false;
32
51
  if (forPath && forPath !== initializedForPath) return false;
@@ -119,7 +138,11 @@ async function loadSubFile(
119
138
  }
120
139
 
121
140
  function buildSkillDescription(): string {
122
- if (cachedSkillList.length === 0) {
141
+ const enabledSkills = filterDiscoveredSkills(
142
+ cachedSkillList,
143
+ cachedSkillSettings,
144
+ );
145
+ if (enabledSkills.length === 0) {
123
146
  return 'Load a skill by name to get detailed, task-specific instructions. No skills are currently available.';
124
147
  }
125
148
 
@@ -128,7 +151,7 @@ function buildSkillDescription(): string {
128
151
  // defensively here in case the same name slipped through from different dirs.
129
152
  const seen = new Set<string>();
130
153
  const unique: DiscoveredSkill[] = [];
131
- for (const s of cachedSkillList) {
154
+ for (const s of enabledSkills) {
132
155
  const key = s.name.trim();
133
156
  if (!key || seen.has(key)) continue;
134
157
  seen.add(key);
@@ -136,42 +159,11 @@ function buildSkillDescription(): string {
136
159
  }
137
160
  unique.sort((a, b) => a.name.localeCompare(b.name));
138
161
 
139
- const skillsXml = unique
140
- .map(
141
- (s) =>
142
- `<skill><name>${escapeXml(s.name)}</name><description>${escapeXml(summarizeDescription(s.description))}</description><location>${escapeXml(s.scope)}</location></skill>`,
143
- )
162
+ const catalog = unique
163
+ .map((s) => `- ${s.name}: ${summarizeDescription(s.description)}`)
144
164
  .join('\n');
145
165
 
146
- return `Load a skill by name to get detailed, task-specific instructions.
147
-
148
- <skills_instructions>
149
- When the user's request matches one of the available skills below, call this tool with the skill name to load its full instructions. Skills provide specialized capabilities and domain knowledge.
150
-
151
- How to use skills:
152
- - Invoke with \`skill({ name: "<skill-name>" })\` — only the name is required.
153
- - The response contains the skill's full body plus \`availableFiles\` for sub-files.
154
- - For sub-files: \`skill({ name: "<skill-name>", file: "rules/animations.md" })\`.
155
-
156
- Rules:
157
- - Only invoke skills listed in <available_skills> below.
158
- - Do NOT invoke speculatively. Only call when the user's request clearly matches a skill's description or trigger phrases.
159
- - Do NOT invoke the same skill twice in one turn.
160
- - If a skill response includes \`securityNotices\`, review them — they flag hidden content (HTML comments, invisible characters, etc.) that may not render visibly.
161
- </skills_instructions>
162
-
163
- <available_skills>
164
- ${skillsXml}
165
- </available_skills>`;
166
- }
167
-
168
- function escapeXml(str: string): string {
169
- return String(str)
170
- .replace(/&/g, '&amp;')
171
- .replace(/</g, '&lt;')
172
- .replace(/>/g, '&gt;')
173
- .replace(/"/g, '&quot;')
174
- .replace(/'/g, '&apos;');
166
+ return `Load a skill by name to get detailed, task-specific instructions. Use only when the user's request clearly matches a listed skill. Available skills:\n${catalog}`;
175
167
  }
176
168
 
177
169
  // Condense a SKILL.md description to "what it does + when to use it".
@@ -1,4 +1,9 @@
1
- import type { ProviderId } from './provider';
1
+ import type {
2
+ ModelInfo,
3
+ ProviderCompatibility,
4
+ ProviderId,
5
+ ProviderPromptFamily,
6
+ } from './provider';
2
7
 
3
8
  /**
4
9
  * Configuration scope - where settings are stored
@@ -30,13 +35,33 @@ export type DefaultConfig = {
30
35
  autoCompactThresholdTokens?: number | null;
31
36
  };
32
37
 
33
- export type ProviderSettings = Record<
34
- ProviderId,
35
- {
36
- enabled: boolean;
37
- apiKey?: string;
38
- }
39
- >;
38
+ export type ProviderSettingsEntry = {
39
+ enabled: boolean;
40
+ apiKey?: string;
41
+ apiKeyEnv?: string;
42
+ baseURL?: string;
43
+ label?: string;
44
+ custom?: boolean;
45
+ compatibility?: ProviderCompatibility;
46
+ family?: ProviderPromptFamily;
47
+ models?: Array<string | ModelInfo>;
48
+ allowAnyModel?: boolean;
49
+ modelDiscovery?: {
50
+ type: 'openai-models' | 'ollama';
51
+ };
52
+ };
53
+
54
+ export type ProviderSettings = Record<string, ProviderSettingsEntry>;
55
+
56
+ export type SkillSettings = {
57
+ enabled?: boolean;
58
+ items?: Record<
59
+ string,
60
+ {
61
+ enabled?: boolean;
62
+ }
63
+ >;
64
+ };
40
65
 
41
66
  /**
42
67
  * Path configuration
@@ -55,6 +80,7 @@ export type OttoConfig = {
55
80
  projectRoot: string;
56
81
  defaults: DefaultConfig;
57
82
  providers: ProviderSettings;
83
+ skills?: SkillSettings;
58
84
  paths: PathConfig;
59
85
  debugEnabled?: boolean;
60
86
  debugScopes?: string[];
@@ -1,6 +1,9 @@
1
1
  // Provider types
2
2
  export type {
3
+ BuiltInProviderId,
3
4
  ProviderId,
5
+ ProviderCompatibility,
6
+ ProviderPromptFamily,
4
7
  ProviderFamily,
5
8
  ModelOwner,
6
9
  ModelInfo,
@@ -16,6 +19,7 @@ export type {
16
19
  Scope,
17
20
  DefaultConfig,
18
21
  PathConfig,
22
+ ProviderSettingsEntry,
19
23
  ProviderSettings,
20
24
  OttoConfig,
21
25
  ToolApprovalMode,
@@ -1,19 +1,49 @@
1
1
  /**
2
- * Provider identifiers for supported AI providers
2
+ * Built-in provider identifiers supported directly by otto.
3
3
  */
4
- export type ProviderId =
4
+ export type BuiltInProviderId =
5
5
  | 'openai'
6
6
  | 'anthropic'
7
7
  | 'google'
8
+ | 'ollama-cloud'
8
9
  | 'openrouter'
9
10
  | 'opencode'
10
11
  | 'copilot'
11
- | 'setu'
12
+ | 'ottorouter'
12
13
  | 'zai'
13
14
  | 'zai-coding'
14
15
  | 'moonshot'
15
16
  | 'minimax';
16
17
 
18
+ /**
19
+ * Provider identifiers may be built-in or custom/config-defined.
20
+ */
21
+ export type ProviderId = BuiltInProviderId | (string & {});
22
+
23
+ /**
24
+ * Compatibility protocol used to instantiate a provider client.
25
+ */
26
+ export type ProviderCompatibility =
27
+ | 'openai'
28
+ | 'anthropic'
29
+ | 'google'
30
+ | 'openrouter'
31
+ | 'ollama'
32
+ | 'openai-compatible';
33
+
34
+ /**
35
+ * Prompt/behavior family used for prompts and provider-specific behavior.
36
+ */
37
+ export type ProviderPromptFamily =
38
+ | 'default'
39
+ | 'anthropic'
40
+ | 'openai'
41
+ | 'google'
42
+ | 'moonshot'
43
+ | 'minimax'
44
+ | 'glm'
45
+ | 'openai-compatible';
46
+
17
47
  /**
18
48
  * Provider family for prompt selection
19
49
  */
@@ -78,7 +108,7 @@ export type ModelInfo = {
78
108
  };
79
109
 
80
110
  export type ProviderCatalogEntry = {
81
- id: ProviderId;
111
+ id: BuiltInProviderId;
82
112
  label?: string;
83
113
  env?: string[];
84
114
  npm?: string;