@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 +2 -2
- package/src/auth/src/index.ts +9 -0
- package/src/auth/src/kimi-oauth.ts +196 -0
- package/src/config/src/index.ts +27 -3
- package/src/config/src/manager.ts +13 -1
- package/src/core/src/providers/resolver.ts +5 -3
- package/src/index.ts +19 -2
- package/src/providers/src/catalog-manual.ts +19 -0
- package/src/providers/src/catalog.ts +257 -144
- package/src/providers/src/env.ts +15 -2
- package/src/providers/src/index.ts +9 -2
- package/src/providers/src/model-merge.ts +27 -0
- package/src/providers/src/moonshot-client.ts +116 -8
- package/src/providers/src/registry.ts +54 -15
- package/src/providers/src/utils.ts +23 -10
- package/src/providers/src/validate.ts +10 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ottocode/sdk",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
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",
|
package/src/auth/src/index.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/config/src/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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, ...
|
|
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
|
-
|
|
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
|
|
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 {
|
|
164
|
-
|
|
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
|
}
|