@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.
@@ -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 MoonshotProviderConfig = {
5
+ export type KimiProviderConfig = {
5
6
  apiKey?: string;
6
7
  baseURL?: string;
8
+ oauth?: OAuth;
7
9
  };
8
10
 
9
- export function createMoonshotModel(
10
- model: string,
11
- config?: MoonshotProviderConfig,
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 baseURL = config?.baseURL || entry?.api || 'https://api.moonshot.ai/v1';
15
- const apiKey = config?.apiKey || process.env.MOONSHOT_API_KEY || '';
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: entry?.label ?? 'Moonshot AI',
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 function isBuiltInProviderId(
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
- if (isBuiltInProviderId(provider)) {
119
- const entry = catalog[provider];
135
+ const catalogProvider = resolveBuiltInProviderCatalogId(provider);
136
+ if (catalogProvider) {
137
+ const entry = catalog[catalogProvider];
120
138
  if (!entry) return undefined;
121
- const cachedEntry = getCachedProviderCatalogEntry(provider);
122
- const models = cachedEntry?.models ?? entry.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: settings?.label ?? cachedEntry?.label ?? entry.label ?? provider,
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[provider],
128
- family: BUILTIN_FAMILY[provider],
129
- baseURL: normalizeOptionalText(settings?.baseURL) ?? entry.api,
130
- apiKey: normalizeOptionalText(settings?.apiKey),
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(settings?.apiKeyEnv) ?? providerEnvVar(provider),
162
+ normalizeOptionalText(resolvedSettings?.apiKeyEnv) ??
163
+ providerEnvVar(provider),
133
164
  models,
134
- allowAnyModel: provider === 'ollama-cloud',
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
- if (isBuiltInProviderId(provider)) {
228
- return getUnderlyingProviderKey(provider, model) ?? definition.family;
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 { isBuiltInProviderId } from './registry.ts';
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 = PREFERRED_FAST_MODELS[provider] ?? [];
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 entry = isBuiltInProviderId(provider) ? catalog[provider] : undefined;
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 entry = isBuiltInProviderId(provider) ? catalog[provider] : undefined;
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
- return (
250
- getCachedProviderCatalogEntry(provider)?.models ??
251
- (isBuiltInProviderId(provider) ? catalog[provider]?.models : undefined) ??
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(providerId)?.models ?? [];
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 builtInEntry = isBuiltInProviderId(p) ? catalog[p] : undefined;
73
- if (!builtInEntry && !getCachedProviderCatalogEntry(p)) {
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);