@ottocode/sdk 0.1.311 → 0.1.313

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/sdk",
3
- "version": "0.1.311",
3
+ "version": "0.1.313",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "nitishxyz",
6
6
  "license": "MIT",
@@ -109,7 +109,7 @@
109
109
  "@modelcontextprotocol/sdk": "1.27.1",
110
110
  "@openauthjs/openauth": "0.4.3",
111
111
  "@openrouter/ai-sdk-provider": "1.5.4",
112
- "@ottorouter/ai-sdk": "0.2.6",
112
+ "@ottorouter/ai-sdk": "0.2.7",
113
113
  "@solana/web3.js": "1.98.4",
114
114
  "ai": "6.0.199",
115
115
  "ai-sdk-ollama": "3.8.3",
@@ -97,6 +97,15 @@ export {
97
97
  type XaiOAuthTokens,
98
98
  } from './xai-oauth.ts';
99
99
 
100
+ export {
101
+ refreshKimiToken,
102
+ requestKimiDeviceCode,
103
+ pollKimiDeviceCodeOnce,
104
+ type KimiOAuthTokens,
105
+ type KimiDeviceCodeResponse,
106
+ type KimiDevicePollResult,
107
+ } from './kimi-oauth.ts';
108
+
100
109
  export {
101
110
  generateWallet,
102
111
  importWallet,
@@ -0,0 +1,196 @@
1
+ const KIMI_CODE_OAUTH_CLIENT_ID = '17e5f671-d194-4dfb-9706-5516cb48c098';
2
+
3
+ function kimiOAuthHost(): string {
4
+ return (
5
+ process.env.KIMI_CODE_OAUTH_HOST ??
6
+ process.env.KIMI_OAUTH_HOST ??
7
+ 'https://auth.kimi.com'
8
+ );
9
+ }
10
+
11
+ /** Kimi Code OAuth tokens normalized to otto's OAuth shape (expires in epoch ms). */
12
+ export type KimiOAuthTokens = {
13
+ access: string;
14
+ refresh: string;
15
+ expires: number;
16
+ scopes?: string;
17
+ };
18
+
19
+ /** Response from a Kimi Code OAuth device authorization request. */
20
+ export type KimiDeviceCodeResponse = {
21
+ userCode: string;
22
+ deviceCode: string;
23
+ verificationUri: string;
24
+ interval: number;
25
+ expiresIn: number | null;
26
+ };
27
+
28
+ /** Result of a single Kimi device-code token poll attempt. */
29
+ export type KimiDevicePollResult =
30
+ | { status: 'complete'; tokens: KimiOAuthTokens }
31
+ | { status: 'pending' }
32
+ | { status: 'error'; error: string };
33
+
34
+ /**
35
+ * Request a Kimi Code OAuth device authorization code.
36
+ *
37
+ * Mirrors the kimi-cli device flow: form-encoded POST to
38
+ * `https://auth.kimi.com/api/oauth/device_authorization` with `client_id`.
39
+ */
40
+ export async function requestKimiDeviceCode(): Promise<KimiDeviceCodeResponse> {
41
+ const response = await fetch(
42
+ `${kimiOAuthHost().replace(/\/$/, '')}/api/oauth/device_authorization`,
43
+ {
44
+ method: 'POST',
45
+ headers: {
46
+ 'Content-Type': 'application/x-www-form-urlencoded',
47
+ Accept: 'application/json',
48
+ },
49
+ body: new URLSearchParams({
50
+ client_id: KIMI_CODE_OAUTH_CLIENT_ID,
51
+ }).toString(),
52
+ },
53
+ );
54
+ const data = (await response.json().catch(() => ({}))) as Record<
55
+ string,
56
+ unknown
57
+ >;
58
+ if (!response.ok) {
59
+ throw new Error(
60
+ `Kimi OAuth device authorization failed (${response.status})`,
61
+ );
62
+ }
63
+ const userCode = data.user_code;
64
+ const deviceCode = data.device_code;
65
+ const verificationUriComplete = data.verification_uri_complete;
66
+ if (
67
+ typeof userCode !== 'string' ||
68
+ typeof deviceCode !== 'string' ||
69
+ typeof verificationUriComplete !== 'string'
70
+ ) {
71
+ throw new Error('Kimi OAuth device authorization response was incomplete.');
72
+ }
73
+ return {
74
+ userCode,
75
+ deviceCode,
76
+ verificationUri: verificationUriComplete,
77
+ interval: Number(data.interval ?? 5),
78
+ expiresIn:
79
+ data.expires_in === undefined || data.expires_in === null
80
+ ? null
81
+ : Number(data.expires_in),
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Poll the Kimi Code OAuth token endpoint once for a device-code grant.
87
+ *
88
+ * Returns `pending` while authorization is outstanding (including
89
+ * `slow_down`), `complete` with normalized tokens on success, and `error`
90
+ * for terminal failures such as `expired_token` or `access_denied`.
91
+ */
92
+ export async function pollKimiDeviceCodeOnce(
93
+ deviceCode: string,
94
+ ): Promise<KimiDevicePollResult> {
95
+ const response = await fetch(
96
+ `${kimiOAuthHost().replace(/\/$/, '')}/api/oauth/token`,
97
+ {
98
+ method: 'POST',
99
+ headers: {
100
+ 'Content-Type': 'application/x-www-form-urlencoded',
101
+ Accept: 'application/json',
102
+ },
103
+ body: new URLSearchParams({
104
+ client_id: KIMI_CODE_OAUTH_CLIENT_ID,
105
+ device_code: deviceCode,
106
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
107
+ }).toString(),
108
+ },
109
+ );
110
+ const data = (await response.json().catch(() => ({}))) as Record<
111
+ string,
112
+ unknown
113
+ >;
114
+ if (response.ok && typeof data.access_token === 'string') {
115
+ const expiresIn = Number(data.expires_in ?? 0);
116
+ return {
117
+ status: 'complete',
118
+ tokens: {
119
+ access: data.access_token,
120
+ refresh:
121
+ typeof data.refresh_token === 'string' ? data.refresh_token : '',
122
+ expires: Date.now() + expiresIn * 1000,
123
+ scopes: typeof data.scope === 'string' ? data.scope : undefined,
124
+ },
125
+ };
126
+ }
127
+ const errorCode =
128
+ typeof data.error === 'string' ? data.error : 'unknown_error';
129
+ if (errorCode === 'authorization_pending' || errorCode === 'slow_down') {
130
+ return { status: 'pending' };
131
+ }
132
+ if (errorCode === 'expired_token') {
133
+ return { status: 'error', error: 'Kimi OAuth code expired.' };
134
+ }
135
+ if (errorCode === 'access_denied') {
136
+ return { status: 'error', error: 'Kimi OAuth access denied.' };
137
+ }
138
+ return {
139
+ status: 'error',
140
+ error: `Kimi OAuth token polling failed: ${errorCode}`,
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Refresh a Kimi Code OAuth access token using the refresh_token grant.
146
+ *
147
+ * Mirrors the official kimi-cli flow: form-encoded POST to
148
+ * `https://auth.kimi.com/api/oauth/token` with `client_id`,
149
+ * `grant_type=refresh_token`, and `refresh_token`. Kimi rotates refresh
150
+ * tokens on every refresh, so callers must persist the returned tokens.
151
+ */
152
+ export async function refreshKimiToken(
153
+ refreshToken: string,
154
+ ): Promise<KimiOAuthTokens> {
155
+ const response = await fetch(
156
+ `${kimiOAuthHost().replace(/\/$/, '')}/api/oauth/token`,
157
+ {
158
+ method: 'POST',
159
+ headers: {
160
+ 'Content-Type': 'application/x-www-form-urlencoded',
161
+ Accept: 'application/json',
162
+ },
163
+ body: new URLSearchParams({
164
+ client_id: KIMI_CODE_OAUTH_CLIENT_ID,
165
+ grant_type: 'refresh_token',
166
+ refresh_token: refreshToken,
167
+ }).toString(),
168
+ },
169
+ );
170
+ const data = (await response.json().catch(() => ({}))) as Record<
171
+ string,
172
+ unknown
173
+ >;
174
+ if (!response.ok || typeof data.access_token !== 'string') {
175
+ const description =
176
+ typeof data.error_description === 'string'
177
+ ? data.error_description
178
+ : `HTTP ${response.status}`;
179
+ if (response.status === 401 || response.status === 403) {
180
+ throw new Error(
181
+ `Kimi OAuth refresh token rejected (${description}). Run \`otto auth login kimi\` again.`,
182
+ );
183
+ }
184
+ throw new Error(`Kimi OAuth token refresh failed: ${description}`);
185
+ }
186
+ const expiresIn = Number(data.expires_in ?? 0);
187
+ return {
188
+ access: data.access_token,
189
+ refresh:
190
+ typeof data.refresh_token === 'string' && data.refresh_token
191
+ ? data.refresh_token
192
+ : refreshToken,
193
+ expires: Date.now() + expiresIn * 1000,
194
+ scopes: typeof data.scope === 'string' ? data.scope : undefined,
195
+ };
196
+ }
@@ -51,6 +51,12 @@ const DEFAULTS: {
51
51
  providers: DEFAULT_PROVIDER_SETTINGS,
52
52
  };
53
53
 
54
+ const LOCAL_DEFAULT_OVERRIDE_KEYS = [
55
+ 'agent',
56
+ 'provider',
57
+ 'model',
58
+ ] satisfies Array<keyof OttoConfig['defaults']>;
59
+
54
60
  export async function loadConfig(
55
61
  projectRootInput?: string,
56
62
  ): Promise<OttoConfig> {
@@ -72,7 +78,7 @@ export async function loadConfig(
72
78
  DEFAULTS,
73
79
  globalCfg,
74
80
  globalSkillsCfg ? { skills: globalSkillsCfg } : undefined,
75
- omitGlobalOnlySettings(projectCfg),
81
+ filterProjectConfig(projectCfg),
76
82
  );
77
83
 
78
84
  await ensureDir(dataDir);
@@ -97,14 +103,32 @@ export async function loadConfig(
97
103
 
98
104
  type JsonObject = Record<string, unknown>;
99
105
 
100
- function omitGlobalOnlySettings(
106
+ function filterProjectConfig(
101
107
  config: JsonObject | undefined,
102
108
  ): JsonObject | undefined {
103
109
  if (!config) return undefined;
104
- const { providers: _providers, skills: _skills, ...rest } = config;
110
+ const { providers: _providers, skills: _skills, defaults, ...rest } = config;
111
+ const localDefaults = pickLocalDefaults(defaults);
112
+ if (localDefaults) {
113
+ return { ...rest, defaults: localDefaults };
114
+ }
105
115
  return rest;
106
116
  }
107
117
 
118
+ function pickLocalDefaults(defaults: unknown): JsonObject | undefined {
119
+ if (!defaults || typeof defaults !== 'object' || Array.isArray(defaults)) {
120
+ return undefined;
121
+ }
122
+ const source = defaults as JsonObject;
123
+ const picked: JsonObject = {};
124
+ for (const key of LOCAL_DEFAULT_OVERRIDE_KEYS) {
125
+ if (Object.hasOwn(source, key)) {
126
+ picked[key] = source[key];
127
+ }
128
+ }
129
+ return Object.keys(picked).length > 0 ? picked : undefined;
130
+ }
131
+
108
132
  async function readJsonOptional(file: string): Promise<JsonObject | undefined> {
109
133
  const f = Bun.file(file);
110
134
  if (!(await f.exists())) return undefined;
@@ -28,6 +28,8 @@ import {
28
28
 
29
29
  export type Scope = 'global' | 'local';
30
30
 
31
+ const LOCAL_DEFAULT_UPDATE_KEYS = new Set(['agent', 'provider', 'model']);
32
+
31
33
  export type DebugConfig = {
32
34
  enabled: boolean;
33
35
  scopes: string[];
@@ -95,6 +97,16 @@ export async function writeDefaults(
95
97
  }>,
96
98
  projectRoot?: string,
97
99
  ) {
100
+ const scopedUpdates =
101
+ scope === 'local'
102
+ ? (Object.fromEntries(
103
+ Object.entries(updates).filter(([key]) =>
104
+ LOCAL_DEFAULT_UPDATE_KEYS.has(key),
105
+ ),
106
+ ) as typeof updates)
107
+ : updates;
108
+ if (Object.keys(scopedUpdates).length === 0) return;
109
+
98
110
  const filePath = getConfigFilePath(scope, projectRoot);
99
111
  const existing = await readJsonFile(filePath);
100
112
  const prevDefaults =
@@ -103,7 +115,7 @@ export async function writeDefaults(
103
115
  : {};
104
116
  const next = {
105
117
  ...existing,
106
- defaults: { ...prevDefaults, ...updates },
118
+ defaults: { ...prevDefaults, ...scopedUpdates },
107
119
  };
108
120
  await writeConfigFile(filePath, next);
109
121
  }
@@ -7,7 +7,7 @@ import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
7
7
  import {
8
8
  catalog,
9
9
  createMinimaxModel,
10
- createMoonshotModel,
10
+ createKimiModel,
11
11
  createOttoRouterModel,
12
12
  createOpenAIOAuthModel,
13
13
  createXaiModel,
@@ -34,6 +34,7 @@ export type ProviderName =
34
34
  | 'zai'
35
35
  | 'zai-coding'
36
36
  | 'moonshot'
37
+ | 'kimi'
37
38
  | 'minimax';
38
39
 
39
40
  export type ModelConfig = {
@@ -221,10 +222,11 @@ export async function resolveModel(
221
222
  });
222
223
  }
223
224
 
224
- if (provider === 'moonshot') {
225
- return createMoonshotModel(model, {
225
+ if (provider === 'moonshot' || provider === 'kimi') {
226
+ return createKimiModel(model, {
226
227
  apiKey: config.apiKey,
227
228
  baseURL: config.baseURL,
229
+ oauth: config.oauth,
228
230
  });
229
231
  }
230
232
 
package/src/index.ts CHANGED
@@ -160,8 +160,15 @@ export {
160
160
  export type { OpenRouterProviderConfig } from './providers/src/index.ts';
161
161
  export { createOpencodeModel } from './providers/src/index.ts';
162
162
  export type { OpencodeProviderConfig } from './providers/src/index.ts';
163
- export { createMoonshotModel } from './providers/src/index.ts';
164
- export type { MoonshotProviderConfig } from './providers/src/index.ts';
163
+ export {
164
+ createKimiModel,
165
+ createMoonshotModel,
166
+ readKimiApiKeyFromEnv,
167
+ } from './providers/src/index.ts';
168
+ export type {
169
+ KimiProviderConfig,
170
+ MoonshotProviderConfig,
171
+ } from './providers/src/index.ts';
165
172
  export { createMinimaxModel } from './providers/src/index.ts';
166
173
  export type { MinimaxProviderConfig } from './providers/src/index.ts';
167
174
  export {
@@ -211,6 +218,16 @@ export {
211
218
  readGrokCliAuth,
212
219
  } from './auth/src/index.ts';
213
220
  export type { XaiOAuthResult, XaiOAuthTokens } from './auth/src/index.ts';
221
+ export {
222
+ refreshKimiToken,
223
+ requestKimiDeviceCode,
224
+ pollKimiDeviceCodeOnce,
225
+ } from './auth/src/index.ts';
226
+ export type {
227
+ KimiOAuthTokens,
228
+ KimiDeviceCodeResponse,
229
+ KimiDevicePollResult,
230
+ } from './auth/src/index.ts';
214
231
  export {
215
232
  generateWallet,
216
233
  importWallet,
@@ -154,6 +154,21 @@ export function appendXaiGrokCliModels<T extends { models: ModelInfo[] }>(
154
154
  return { ...entry, models: [...mergedModels, ...missingModels] };
155
155
  }
156
156
 
157
+ export function applyOfficialKimiCatalogMetadata<
158
+ T extends ProviderCatalogEntry,
159
+ >(entry: T | undefined): T | undefined {
160
+ if (!entry) return undefined;
161
+ const env = Array.from(
162
+ new Set(['KIMI_API_KEY', 'MOONSHOT_API_KEY', ...(entry.env ?? [])]),
163
+ );
164
+ return {
165
+ ...entry,
166
+ label: entry.label === 'Moonshot AI' ? 'Kimi' : entry.label,
167
+ env,
168
+ doc: 'https://platform.kimi.ai/docs/api/overview.md',
169
+ };
170
+ }
171
+
157
172
  export function mergeManualCatalog(
158
173
  base: CatalogMap,
159
174
  ): Record<BuiltInProviderId, ProviderCatalogEntry> {
@@ -167,6 +182,10 @@ export function mergeManualCatalog(
167
182
  if (xaiEntry) {
168
183
  merged.xai = xaiEntry;
169
184
  }
185
+ const moonshotEntry = applyOfficialKimiCatalogMetadata(merged.moonshot);
186
+ if (moonshotEntry) {
187
+ merged.moonshot = moonshotEntry;
188
+ }
170
189
  if (manualEntry) {
171
190
  merged[OTTOROUTER_ID] = manualEntry;
172
191
  }