@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
|
@@ -1,25 +1,133 @@
|
|
|
1
1
|
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
2
|
+
import type { OAuth } from '../../types/src/index.ts';
|
|
2
3
|
import { catalog } from './catalog-merged.ts';
|
|
3
4
|
|
|
4
|
-
export type
|
|
5
|
+
export type KimiProviderConfig = {
|
|
5
6
|
apiKey?: string;
|
|
6
7
|
baseURL?: string;
|
|
8
|
+
oauth?: OAuth;
|
|
7
9
|
};
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
) {
|
|
11
|
+
/** @deprecated Use `KimiProviderConfig` */
|
|
12
|
+
export type MoonshotProviderConfig = KimiProviderConfig;
|
|
13
|
+
|
|
14
|
+
export function readKimiApiKeyFromEnv(): string {
|
|
15
|
+
return process.env.KIMI_API_KEY || process.env.MOONSHOT_API_KEY || '';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Kimi/Moonshot streaming responses report token usage on the final chunk's
|
|
20
|
+
* `choices[0].usage` instead of the OpenAI-standard top-level `usage` field.
|
|
21
|
+
* The AI SDK openai-compatible parser only reads top-level `usage`, so we
|
|
22
|
+
* hoist choice-level usage to the top level when it is missing.
|
|
23
|
+
*/
|
|
24
|
+
export function hoistKimiSseUsage(line: string): string {
|
|
25
|
+
const hasCarriageReturn = line.endsWith('\r');
|
|
26
|
+
const raw = hasCarriageReturn ? line.slice(0, -1) : line;
|
|
27
|
+
if (!raw.startsWith('data:')) return line;
|
|
28
|
+
const payload = raw.slice(5).trim();
|
|
29
|
+
if (!payload || payload === '[DONE]') return line;
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(payload) as {
|
|
32
|
+
usage?: unknown;
|
|
33
|
+
choices?: Array<{ usage?: unknown } | null>;
|
|
34
|
+
};
|
|
35
|
+
if (!parsed || typeof parsed !== 'object' || parsed.usage != null) {
|
|
36
|
+
return line;
|
|
37
|
+
}
|
|
38
|
+
const choiceUsage = Array.isArray(parsed.choices)
|
|
39
|
+
? parsed.choices.find((choice) => choice?.usage != null)?.usage
|
|
40
|
+
: undefined;
|
|
41
|
+
if (choiceUsage == null) return line;
|
|
42
|
+
parsed.usage = choiceUsage;
|
|
43
|
+
return `data: ${JSON.stringify(parsed)}${hasCarriageReturn ? '\r' : ''}`;
|
|
44
|
+
} catch {
|
|
45
|
+
return line;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Wraps fetch so Kimi SSE chunks carrying `choices[0].usage` are rewritten to
|
|
51
|
+
* expose a top-level `usage` field the AI SDK can parse.
|
|
52
|
+
*/
|
|
53
|
+
export function createKimiUsageFetch(
|
|
54
|
+
baseFetch: typeof fetch = fetch,
|
|
55
|
+
): typeof fetch {
|
|
56
|
+
const wrappedFetch = async (
|
|
57
|
+
input: Parameters<typeof fetch>[0],
|
|
58
|
+
init?: Parameters<typeof fetch>[1],
|
|
59
|
+
): Promise<Response> => {
|
|
60
|
+
const response = await baseFetch(input, init);
|
|
61
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
62
|
+
if (
|
|
63
|
+
!response.ok ||
|
|
64
|
+
!response.body ||
|
|
65
|
+
!contentType.includes('text/event-stream')
|
|
66
|
+
) {
|
|
67
|
+
return response;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const decoder = new TextDecoder();
|
|
71
|
+
const encoder = new TextEncoder();
|
|
72
|
+
let buffered = '';
|
|
73
|
+
const transform = new TransformStream<Uint8Array, Uint8Array>({
|
|
74
|
+
transform(chunk, controller) {
|
|
75
|
+
buffered += decoder.decode(chunk, { stream: true });
|
|
76
|
+
const lines = buffered.split('\n');
|
|
77
|
+
buffered = lines.pop() ?? '';
|
|
78
|
+
for (const line of lines) {
|
|
79
|
+
controller.enqueue(encoder.encode(`${hoistKimiSseUsage(line)}\n`));
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
flush(controller) {
|
|
83
|
+
buffered += decoder.decode();
|
|
84
|
+
if (buffered.length) {
|
|
85
|
+
controller.enqueue(encoder.encode(hoistKimiSseUsage(buffered)));
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const headers = new Headers(response.headers);
|
|
91
|
+
headers.delete('content-length');
|
|
92
|
+
headers.delete('content-encoding');
|
|
93
|
+
return new Response(response.body.pipeThrough(transform), {
|
|
94
|
+
status: response.status,
|
|
95
|
+
statusText: response.statusText,
|
|
96
|
+
headers,
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
return wrappedFetch as typeof fetch;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function createKimiModel(model: string, config?: KimiProviderConfig) {
|
|
13
103
|
const entry = catalog.moonshot;
|
|
14
|
-
const
|
|
15
|
-
const
|
|
104
|
+
const oauthAccess = config?.oauth?.access;
|
|
105
|
+
const defaultApiBaseURL = entry?.api ?? 'https://api.moonshot.ai/v1';
|
|
106
|
+
const configuredBaseURL = config?.baseURL;
|
|
107
|
+
const kimiCodeBaseURL =
|
|
108
|
+
process.env.KIMI_CODE_BASE_URL ?? 'https://api.kimi.com/coding/v1';
|
|
109
|
+
const baseURL =
|
|
110
|
+
oauthAccess &&
|
|
111
|
+
(!configuredBaseURL || configuredBaseURL === defaultApiBaseURL)
|
|
112
|
+
? kimiCodeBaseURL
|
|
113
|
+
: (configuredBaseURL ?? defaultApiBaseURL);
|
|
114
|
+
const apiKey = oauthAccess || config?.apiKey || readKimiApiKeyFromEnv();
|
|
16
115
|
const headers = apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined;
|
|
17
116
|
|
|
18
117
|
const instance = createOpenAICompatible({
|
|
19
|
-
name:
|
|
118
|
+
name: 'Kimi',
|
|
20
119
|
baseURL,
|
|
21
120
|
headers,
|
|
121
|
+
fetch: createKimiUsageFetch(),
|
|
22
122
|
});
|
|
23
123
|
|
|
24
124
|
return instance(model);
|
|
25
125
|
}
|
|
126
|
+
|
|
127
|
+
/** @deprecated Use `createKimiModel` */
|
|
128
|
+
export function createMoonshotModel(
|
|
129
|
+
model: string,
|
|
130
|
+
config?: MoonshotProviderConfig,
|
|
131
|
+
) {
|
|
132
|
+
return createKimiModel(model, config);
|
|
133
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { catalog } from './catalog-merged.ts';
|
|
2
|
-
import { providerEnvVar } from './env.ts';
|
|
2
|
+
import { providerEnvVar, readEnvKey } from './env.ts';
|
|
3
3
|
import { getCachedProviderCatalogEntry } from './model-catalog-cache.ts';
|
|
4
|
+
import { mergeModelLists } from './model-merge.ts';
|
|
4
5
|
import { getUnderlyingProviderKey, providerIds } from './utils.ts';
|
|
5
6
|
import type {
|
|
6
7
|
BuiltInProviderId,
|
|
@@ -94,7 +95,9 @@ function resolveCustomFamily(
|
|
|
94
95
|
return settings.family ?? 'default';
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
export
|
|
98
|
+
export const KIMI_PROVIDER_ALIAS = 'kimi' as const;
|
|
99
|
+
|
|
100
|
+
function isCatalogBuiltInProviderId(
|
|
98
101
|
value: unknown,
|
|
99
102
|
): value is BuiltInProviderId {
|
|
100
103
|
return (
|
|
@@ -103,6 +106,20 @@ export function isBuiltInProviderId(
|
|
|
103
106
|
);
|
|
104
107
|
}
|
|
105
108
|
|
|
109
|
+
export function resolveBuiltInProviderCatalogId(
|
|
110
|
+
provider: ProviderId,
|
|
111
|
+
): BuiltInProviderId | undefined {
|
|
112
|
+
if (provider === KIMI_PROVIDER_ALIAS) return 'moonshot';
|
|
113
|
+
if (isCatalogBuiltInProviderId(provider)) return provider;
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function isBuiltInProviderId(
|
|
118
|
+
value: unknown,
|
|
119
|
+
): value is BuiltInProviderId {
|
|
120
|
+
return isCatalogBuiltInProviderId(value) || value === KIMI_PROVIDER_ALIAS;
|
|
121
|
+
}
|
|
122
|
+
|
|
106
123
|
export function getProviderSettings(
|
|
107
124
|
cfg: OttoConfig,
|
|
108
125
|
provider: ProviderId,
|
|
@@ -115,23 +132,37 @@ export function getProviderDefinition(
|
|
|
115
132
|
provider: ProviderId,
|
|
116
133
|
): ResolvedProviderDefinition | undefined {
|
|
117
134
|
const settings = getProviderSettings(cfg, provider);
|
|
118
|
-
|
|
119
|
-
|
|
135
|
+
const catalogProvider = resolveBuiltInProviderCatalogId(provider);
|
|
136
|
+
if (catalogProvider) {
|
|
137
|
+
const entry = catalog[catalogProvider];
|
|
120
138
|
if (!entry) return undefined;
|
|
121
|
-
const cachedEntry = getCachedProviderCatalogEntry(
|
|
122
|
-
const models = cachedEntry?.models
|
|
139
|
+
const cachedEntry = getCachedProviderCatalogEntry(catalogProvider);
|
|
140
|
+
const models = mergeModelLists(entry.models, cachedEntry?.models);
|
|
141
|
+
const moonshotSettings =
|
|
142
|
+
provider === KIMI_PROVIDER_ALIAS
|
|
143
|
+
? (getProviderSettings(cfg, 'moonshot') ?? settings)
|
|
144
|
+
: settings;
|
|
145
|
+
const resolvedSettings =
|
|
146
|
+
provider === KIMI_PROVIDER_ALIAS
|
|
147
|
+
? (settings ?? moonshotSettings)
|
|
148
|
+
: settings;
|
|
123
149
|
return {
|
|
124
150
|
id: provider,
|
|
125
|
-
label:
|
|
151
|
+
label:
|
|
152
|
+
resolvedSettings?.label ??
|
|
153
|
+
(provider === KIMI_PROVIDER_ALIAS
|
|
154
|
+
? 'Kimi'
|
|
155
|
+
: (cachedEntry?.label ?? entry.label ?? provider)),
|
|
126
156
|
source: 'built-in',
|
|
127
|
-
compatibility: BUILTIN_COMPATIBILITY[
|
|
128
|
-
family: BUILTIN_FAMILY[
|
|
129
|
-
baseURL: normalizeOptionalText(
|
|
130
|
-
apiKey: normalizeOptionalText(
|
|
157
|
+
compatibility: BUILTIN_COMPATIBILITY[catalogProvider],
|
|
158
|
+
family: BUILTIN_FAMILY[catalogProvider],
|
|
159
|
+
baseURL: normalizeOptionalText(resolvedSettings?.baseURL) ?? entry.api,
|
|
160
|
+
apiKey: normalizeOptionalText(resolvedSettings?.apiKey),
|
|
131
161
|
apiKeyEnv:
|
|
132
|
-
normalizeOptionalText(
|
|
162
|
+
normalizeOptionalText(resolvedSettings?.apiKeyEnv) ??
|
|
163
|
+
providerEnvVar(provider),
|
|
133
164
|
models,
|
|
134
|
-
allowAnyModel:
|
|
165
|
+
allowAnyModel: catalogProvider === 'ollama-cloud',
|
|
135
166
|
};
|
|
136
167
|
}
|
|
137
168
|
|
|
@@ -171,6 +202,7 @@ export function getConfiguredProviderIds(
|
|
|
171
202
|
const includeDisabled = options?.includeDisabled === true;
|
|
172
203
|
const ids = new Set<ProviderId>([
|
|
173
204
|
...providerIds,
|
|
205
|
+
KIMI_PROVIDER_ALIAS,
|
|
174
206
|
...Object.keys(cfg.providers),
|
|
175
207
|
cfg.defaults.provider,
|
|
176
208
|
]);
|
|
@@ -224,8 +256,11 @@ export function getConfiguredProviderFamily(
|
|
|
224
256
|
const definition = getProviderDefinition(cfg, provider);
|
|
225
257
|
if (!definition) return null;
|
|
226
258
|
if (definition.source === 'custom') return definition.family;
|
|
227
|
-
|
|
228
|
-
|
|
259
|
+
const catalogProvider = resolveBuiltInProviderCatalogId(provider);
|
|
260
|
+
if (catalogProvider) {
|
|
261
|
+
return (
|
|
262
|
+
getUnderlyingProviderKey(catalogProvider, model) ?? definition.family
|
|
263
|
+
);
|
|
229
264
|
}
|
|
230
265
|
return definition.family;
|
|
231
266
|
}
|
|
@@ -245,6 +280,10 @@ export function getConfiguredProviderApiKey(
|
|
|
245
280
|
const definition = getProviderDefinition(cfg, provider);
|
|
246
281
|
if (!definition) return undefined;
|
|
247
282
|
if (definition.apiKey?.length) return definition.apiKey;
|
|
283
|
+
if (provider === KIMI_PROVIDER_ALIAS || provider === 'moonshot') {
|
|
284
|
+
const envValue = readEnvKey(provider);
|
|
285
|
+
if (envValue?.length) return envValue;
|
|
286
|
+
}
|
|
248
287
|
if (definition.apiKeyEnv?.length) {
|
|
249
288
|
const value = process.env[definition.apiKeyEnv];
|
|
250
289
|
if (value?.length) return value;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { catalog } from './catalog-merged.ts';
|
|
2
2
|
import { getCachedProviderCatalogEntry } from './model-catalog-cache.ts';
|
|
3
|
+
import { mergeModelLists } from './model-merge.ts';
|
|
3
4
|
import type {
|
|
4
5
|
BuiltInProviderId,
|
|
5
6
|
ProviderId,
|
|
@@ -7,7 +8,7 @@ import type {
|
|
|
7
8
|
ModelOwner,
|
|
8
9
|
} from '../../types/src/index.ts';
|
|
9
10
|
import { filterModelsForAuthType } from './oauth-models.ts';
|
|
10
|
-
import {
|
|
11
|
+
import { resolveBuiltInProviderCatalogId } from './registry.ts';
|
|
11
12
|
|
|
12
13
|
export const providerIds = Object.keys(catalog) as BuiltInProviderId[];
|
|
13
14
|
|
|
@@ -44,18 +45,25 @@ const PREFERRED_FAST_MODELS: Partial<Record<ProviderId, string[]>> = {
|
|
|
44
45
|
xai: ['grok-code-fast-1', 'grok-4-fast'],
|
|
45
46
|
zai: ['glm-4.5-flash'],
|
|
46
47
|
copilot: ['gpt-4.1-mini'],
|
|
48
|
+
moonshot: ['kimi-k2-turbo-preview'],
|
|
47
49
|
};
|
|
48
50
|
|
|
49
51
|
const PREFERRED_FAST_MODELS_OAUTH: Partial<Record<ProviderId, string[]>> = {
|
|
50
52
|
openai: ['gpt-5.4-mini'],
|
|
51
53
|
anthropic: ['claude-haiku-4-5'],
|
|
54
|
+
moonshot: ['kimi-k2.7-code'],
|
|
52
55
|
};
|
|
53
56
|
|
|
57
|
+
function preferredFastModelKey(provider: ProviderId): ProviderId {
|
|
58
|
+
return resolveBuiltInProviderCatalogId(provider) ?? provider;
|
|
59
|
+
}
|
|
60
|
+
|
|
54
61
|
export function getFastModel(provider: ProviderId): string | undefined {
|
|
55
62
|
const providerModels = getProviderModels(provider);
|
|
56
63
|
if (!providerModels.length) return undefined;
|
|
57
64
|
|
|
58
|
-
const preferred =
|
|
65
|
+
const preferred =
|
|
66
|
+
PREFERRED_FAST_MODELS[preferredFastModelKey(provider)] ?? [];
|
|
59
67
|
for (const modelId of preferred) {
|
|
60
68
|
if (providerModels.some((m) => m.id === modelId)) {
|
|
61
69
|
return modelId;
|
|
@@ -85,7 +93,7 @@ export function getFastModelForAuth(
|
|
|
85
93
|
|
|
86
94
|
const preferredMap =
|
|
87
95
|
authType === 'oauth' ? PREFERRED_FAST_MODELS_OAUTH : PREFERRED_FAST_MODELS;
|
|
88
|
-
const preferred = preferredMap[provider] ?? [];
|
|
96
|
+
const preferred = preferredMap[preferredFastModelKey(provider)] ?? [];
|
|
89
97
|
for (const modelId of preferred) {
|
|
90
98
|
if (filteredModels.some((m) => m.id === modelId)) {
|
|
91
99
|
return modelId;
|
|
@@ -108,7 +116,8 @@ export function getModelNpmBinding(
|
|
|
108
116
|
provider: ProviderId,
|
|
109
117
|
model: string,
|
|
110
118
|
): string | undefined {
|
|
111
|
-
const
|
|
119
|
+
const catalogProvider = resolveBuiltInProviderCatalogId(provider);
|
|
120
|
+
const entry = catalogProvider ? catalog[catalogProvider] : undefined;
|
|
112
121
|
const modelInfo = getProviderModels(provider).find((m) => m.id === model);
|
|
113
122
|
if (modelInfo?.provider?.npm) return modelInfo.provider.npm;
|
|
114
123
|
if (entry?.npm) return entry.npm;
|
|
@@ -240,17 +249,21 @@ export function getModelInfo(
|
|
|
240
249
|
provider: ProviderId,
|
|
241
250
|
model: string,
|
|
242
251
|
): ModelInfo | undefined {
|
|
243
|
-
const
|
|
252
|
+
const catalogProvider = resolveBuiltInProviderCatalogId(provider);
|
|
253
|
+
const entry = catalogProvider ? catalog[catalogProvider] : undefined;
|
|
244
254
|
if (!entry) return undefined;
|
|
245
255
|
return getProviderModels(provider).find((m) => m.id === model);
|
|
246
256
|
}
|
|
247
257
|
|
|
248
258
|
function getProviderModels(provider: ProviderId): ModelInfo[] {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
259
|
+
const catalogProvider = resolveBuiltInProviderCatalogId(provider);
|
|
260
|
+
const catalogModels = catalogProvider
|
|
261
|
+
? catalog[catalogProvider]?.models
|
|
262
|
+
: undefined;
|
|
263
|
+
const cachedModels = getCachedProviderCatalogEntry(
|
|
264
|
+
catalogProvider ?? provider,
|
|
265
|
+
)?.models;
|
|
266
|
+
return mergeModelLists(catalogModels, cachedModels);
|
|
254
267
|
}
|
|
255
268
|
|
|
256
269
|
export function modelSupportsReasoning(
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { catalog } from './catalog-merged.ts';
|
|
2
2
|
import { getCachedProviderCatalogEntry } from './model-catalog-cache.ts';
|
|
3
|
+
import { mergeModelLists } from './model-merge.ts';
|
|
3
4
|
import type { OttoConfig, ProviderId } from '../../types/src/index.ts';
|
|
4
5
|
import {
|
|
5
6
|
getProviderDefinition,
|
|
6
7
|
hasConfiguredModel,
|
|
7
|
-
isBuiltInProviderId,
|
|
8
8
|
providerAllowsAnyModel,
|
|
9
|
+
resolveBuiltInProviderCatalogId,
|
|
9
10
|
} from './registry.ts';
|
|
10
11
|
|
|
11
12
|
export type CapabilityRequest = {
|
|
@@ -28,7 +29,9 @@ export function validateProviderModel(
|
|
|
28
29
|
if (cfg) {
|
|
29
30
|
const definition = getProviderDefinition(cfg, providerId);
|
|
30
31
|
const cachedModels =
|
|
31
|
-
getCachedProviderCatalogEntry(
|
|
32
|
+
getCachedProviderCatalogEntry(
|
|
33
|
+
resolveBuiltInProviderCatalogId(providerId) ?? providerId,
|
|
34
|
+
)?.models ?? [];
|
|
32
35
|
if (!definition) {
|
|
33
36
|
if (!cachedModels.length) {
|
|
34
37
|
throw new Error(`Provider not supported: ${providerId}`);
|
|
@@ -69,12 +72,13 @@ export function validateProviderModel(
|
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
const p = providerId;
|
|
72
|
-
const
|
|
73
|
-
|
|
75
|
+
const catalogProvider = resolveBuiltInProviderCatalogId(p);
|
|
76
|
+
const builtInEntry = catalogProvider ? catalog[catalogProvider] : undefined;
|
|
77
|
+
const cachedEntry = getCachedProviderCatalogEntry(catalogProvider ?? p);
|
|
78
|
+
if (!builtInEntry && !cachedEntry) {
|
|
74
79
|
throw new Error(`Provider not supported: ${providerId}`);
|
|
75
80
|
}
|
|
76
|
-
const models =
|
|
77
|
-
getCachedProviderCatalogEntry(p)?.models ?? builtInEntry?.models ?? [];
|
|
81
|
+
const models = mergeModelLists(builtInEntry?.models, cachedEntry?.models);
|
|
78
82
|
const entry = models.find((m: { id: string }) => m.id === modelId);
|
|
79
83
|
if (!entry) {
|
|
80
84
|
throwModelNotFound(providerId, modelId, models);
|