diff --git a/packages/ai/cohere/src/index.ts b/packages/ai/cohere/src/index.ts index 6d8a13b0..7740d0f7 100644 --- a/packages/ai/cohere/src/index.ts +++ b/packages/ai/cohere/src/index.ts @@ -4,27 +4,65 @@ interface Config { baseUrl?: string; } +const DEFAULT_BASE = "https://api.cohere.ai/compatibility"; + +function chatCompletionsUrl(baseUrl: string): string { + const base = baseUrl.replace(/\/+$/, ''); + return base.endsWith('/v1') ? `${base}/chat/completions` : `${base}/v1/chat/completions`; +} + export default defineAi({ - id: 'ai-cohere', - label: 'Cohere', - defaultModel: 'command-r-plus', - models: ['command-r-plus'], - - async generate(ctx, prompt, _opts, _config) { - const apiKey = ctx.secret('COHERE_API_KEY'); - if (!apiKey) throw new Error('COHERE_API_KEY not in vault — run `sh1pt promote ai setup`'); - ctx.log(`[stub] ai-cohere · ${prompt.length} chars in — integration pending`); - return { text: '[stub — ai-cohere integration not yet implemented]', model: 'command-r-plus' }; + id: "ai-cohere", + label: "Cohere", + defaultModel: "command-a-plus-05-2026", + models: ["command-a-plus-05-2026", "command-a-03-2025", "command-r-plus", "command-r"], + + async generate(ctx, prompt, opts, config) { + const apiKey = ctx.secret("COHERE_API_KEY"); + if (!apiKey) throw new Error('COHERE_API_KEY not in vault - run `sh1pt promote ai setup`'); + const model = opts.model ?? "command-a-plus-05-2026"; + ctx.log(`ai-cohere · model=${model} · ${prompt.length} chars in`); + if (ctx.dryRun) return { text: '[dry-run]', model }; + + const messages: Array<{ role: string; content: string }> = []; + if (opts.system) messages.push({ role: 'system', content: opts.system }); + messages.push({ role: 'user', content: prompt }); + + const res = await fetch(chatCompletionsUrl(config.baseUrl ?? DEFAULT_BASE), { + method: 'POST', + headers: { + authorization: `Bearer ${apiKey}`, + 'content-type': 'application/json', + }, + body: JSON.stringify({ + model, + messages, + ...(opts.maxTokens !== undefined ? { max_tokens: opts.maxTokens } : {}), + ...(opts.temperature !== undefined ? { temperature: opts.temperature } : {}), + ...opts.extra, + }), + }); + if (!res.ok) throw new Error(`Cohere ${res.status}: ${(await res.text()).slice(0, 200)}`); + const data = (await res.json()) as { + choices: Array<{ message?: { content?: string } }>; + model?: string; + usage?: { prompt_tokens?: number; completion_tokens?: number }; + }; + return { + text: data.choices[0]?.message?.content ?? '', + model: data.model ?? model, + inputTokens: data.usage?.prompt_tokens, + outputTokens: data.usage?.completion_tokens, + }; }, setup: tokenSetup({ - secretKey: 'COHERE_API_KEY', - label: 'Cohere', - vendorDocUrl: 'https://dashboard.cohere.com', - steps: [ - 'Sign in at https://dashboard.cohere.com and create an API key', - 'Copy the key — usually shown once', - 'Paste below; sh1pt encrypts it in the vault', + secretKey: "COHERE_API_KEY", + label: "Cohere", + vendorDocUrl: "https://docs.cohere.com/docs/compatibility-api", + steps: ["Sign in at https://dashboard.cohere.com and create an API key", "Copy the key - usually shown once", "Paste below; sh1pt encrypts it in the vault"], + fields: [ + { key: 'baseUrl', message: 'OpenAI-compatible base URL (optional; leave blank for the default):' }, ], }), }); diff --git a/packages/ai/kimi/src/index.ts b/packages/ai/kimi/src/index.ts index 6bbf7fd3..4add1398 100644 --- a/packages/ai/kimi/src/index.ts +++ b/packages/ai/kimi/src/index.ts @@ -4,27 +4,65 @@ interface Config { baseUrl?: string; } +const DEFAULT_BASE = "https://api.moonshot.ai"; + +function chatCompletionsUrl(baseUrl: string): string { + const base = baseUrl.replace(/\/+$/, ''); + return base.endsWith('/v1') ? `${base}/chat/completions` : `${base}/v1/chat/completions`; +} + export default defineAi({ - id: 'ai-kimi', - label: 'Kimi (Moonshot)', - defaultModel: 'kimi-k2-0905-preview', - models: ['kimi-k2-0905-preview'], - - async generate(ctx, prompt, _opts, _config) { - const apiKey = ctx.secret('MOONSHOT_API_KEY'); - if (!apiKey) throw new Error('MOONSHOT_API_KEY not in vault — run `sh1pt promote ai setup`'); - ctx.log(`[stub] ai-kimi · ${prompt.length} chars in — integration pending`); - return { text: '[stub — ai-kimi integration not yet implemented]', model: 'kimi-k2-0905-preview' }; + id: "ai-kimi", + label: "Kimi (Moonshot)", + defaultModel: "kimi-k2.6", + models: ["kimi-k2.6", "kimi-k2-0905-preview"], + + async generate(ctx, prompt, opts, config) { + const apiKey = ctx.secret("MOONSHOT_API_KEY"); + if (!apiKey) throw new Error('MOONSHOT_API_KEY not in vault - run `sh1pt promote ai setup`'); + const model = opts.model ?? "kimi-k2.6"; + ctx.log(`ai-kimi · model=${model} · ${prompt.length} chars in`); + if (ctx.dryRun) return { text: '[dry-run]', model }; + + const messages: Array<{ role: string; content: string }> = []; + if (opts.system) messages.push({ role: 'system', content: opts.system }); + messages.push({ role: 'user', content: prompt }); + + const res = await fetch(chatCompletionsUrl(config.baseUrl ?? DEFAULT_BASE), { + method: 'POST', + headers: { + authorization: `Bearer ${apiKey}`, + 'content-type': 'application/json', + }, + body: JSON.stringify({ + model, + messages, + ...(opts.maxTokens !== undefined ? { max_tokens: opts.maxTokens } : {}), + ...(opts.temperature !== undefined ? { temperature: opts.temperature } : {}), + ...opts.extra, + }), + }); + if (!res.ok) throw new Error(`Kimi (Moonshot) ${res.status}: ${(await res.text()).slice(0, 200)}`); + const data = (await res.json()) as { + choices: Array<{ message?: { content?: string } }>; + model?: string; + usage?: { prompt_tokens?: number; completion_tokens?: number }; + }; + return { + text: data.choices[0]?.message?.content ?? '', + model: data.model ?? model, + inputTokens: data.usage?.prompt_tokens, + outputTokens: data.usage?.completion_tokens, + }; }, setup: tokenSetup({ - secretKey: 'MOONSHOT_API_KEY', - label: 'Kimi (Moonshot)', - vendorDocUrl: 'https://platform.moonshot.ai', - steps: [ - 'Sign in at https://platform.moonshot.ai and create an API key', - 'Copy the key — usually shown once', - 'Paste below; sh1pt encrypts it in the vault', + secretKey: "MOONSHOT_API_KEY", + label: "Kimi (Moonshot)", + vendorDocUrl: "https://platform.kimi.ai/docs/api/overview", + steps: ["Sign in at https://platform.kimi.ai and create an API key", "Copy the key - usually shown once", "Paste below; sh1pt encrypts it in the vault"], + fields: [ + { key: 'baseUrl', message: 'OpenAI-compatible base URL (optional; leave blank for the default):' }, ], }), }); diff --git a/packages/ai/novita/src/index.ts b/packages/ai/novita/src/index.ts index 70f3b05a..66a17cad 100644 --- a/packages/ai/novita/src/index.ts +++ b/packages/ai/novita/src/index.ts @@ -4,27 +4,65 @@ interface Config { baseUrl?: string; } +const DEFAULT_BASE = "https://api.novita.ai/openai"; + +function chatCompletionsUrl(baseUrl: string): string { + const base = baseUrl.replace(/\/+$/, ''); + return base.endsWith('/v1') ? `${base}/chat/completions` : `${base}/v1/chat/completions`; +} + export default defineAi({ - id: 'ai-novita', - label: 'NovitaAI', - defaultModel: 'meta-llama/llama-3.3-70b-instruct', - models: ['meta-llama/llama-3.3-70b-instruct'], - - async generate(ctx, prompt, _opts, _config) { - const apiKey = ctx.secret('NOVITA_API_KEY'); - if (!apiKey) throw new Error('NOVITA_API_KEY not in vault — run `sh1pt promote ai setup`'); - ctx.log(`[stub] ai-novita · ${prompt.length} chars in — integration pending`); - return { text: '[stub — ai-novita integration not yet implemented]', model: 'meta-llama/llama-3.3-70b-instruct' }; + id: "ai-novita", + label: "NovitaAI", + defaultModel: "meta-llama/llama-3.3-70b-instruct", + models: ["meta-llama/llama-3.3-70b-instruct", "deepseek/deepseek-v3-0324", "qwen/qwen3-235b-a22b-fp8"], + + async generate(ctx, prompt, opts, config) { + const apiKey = ctx.secret("NOVITA_API_KEY"); + if (!apiKey) throw new Error('NOVITA_API_KEY not in vault - run `sh1pt promote ai setup`'); + const model = opts.model ?? "meta-llama/llama-3.3-70b-instruct"; + ctx.log(`ai-novita · model=${model} · ${prompt.length} chars in`); + if (ctx.dryRun) return { text: '[dry-run]', model }; + + const messages: Array<{ role: string; content: string }> = []; + if (opts.system) messages.push({ role: 'system', content: opts.system }); + messages.push({ role: 'user', content: prompt }); + + const res = await fetch(chatCompletionsUrl(config.baseUrl ?? DEFAULT_BASE), { + method: 'POST', + headers: { + authorization: `Bearer ${apiKey}`, + 'content-type': 'application/json', + }, + body: JSON.stringify({ + model, + messages, + ...(opts.maxTokens !== undefined ? { max_tokens: opts.maxTokens } : {}), + ...(opts.temperature !== undefined ? { temperature: opts.temperature } : {}), + ...opts.extra, + }), + }); + if (!res.ok) throw new Error(`NovitaAI ${res.status}: ${(await res.text()).slice(0, 200)}`); + const data = (await res.json()) as { + choices: Array<{ message?: { content?: string } }>; + model?: string; + usage?: { prompt_tokens?: number; completion_tokens?: number }; + }; + return { + text: data.choices[0]?.message?.content ?? '', + model: data.model ?? model, + inputTokens: data.usage?.prompt_tokens, + outputTokens: data.usage?.completion_tokens, + }; }, setup: tokenSetup({ - secretKey: 'NOVITA_API_KEY', - label: 'NovitaAI', - vendorDocUrl: 'https://novita.ai', - steps: [ - 'Sign in at https://novita.ai and create an API key', - 'Copy the key — usually shown once', - 'Paste below; sh1pt encrypts it in the vault', + secretKey: "NOVITA_API_KEY", + label: "NovitaAI", + vendorDocUrl: "https://novita.ai/docs/api-reference/model-apis-llm-create-chat-completion", + steps: ["Sign in at https://novita.ai and create an API key", "Copy the key - usually shown once", "Paste below; sh1pt encrypts it in the vault"], + fields: [ + { key: 'baseUrl', message: 'OpenAI-compatible base URL (optional; leave blank for the default):' }, ], }), }); diff --git a/packages/ai/parasail/src/index.ts b/packages/ai/parasail/src/index.ts index 88d4be22..d17f5a2c 100644 --- a/packages/ai/parasail/src/index.ts +++ b/packages/ai/parasail/src/index.ts @@ -4,27 +4,65 @@ interface Config { baseUrl?: string; } +const DEFAULT_BASE = "https://api.parasail.io"; + +function chatCompletionsUrl(baseUrl: string): string { + const base = baseUrl.replace(/\/+$/, ''); + return base.endsWith('/v1') ? `${base}/chat/completions` : `${base}/v1/chat/completions`; +} + export default defineAi({ - id: 'ai-parasail', - label: 'Parasail', - defaultModel: 'PARASAIL_API_KEY', - models: ['PARASAIL_API_KEY'], - - async generate(ctx, prompt, _opts, _config) { - const apiKey = ctx.secret('https://parasail.io'); - if (!apiKey) throw new Error('https://parasail.io not in vault — run `sh1pt promote ai setup`'); - ctx.log(`[stub] ai-parasail · ${prompt.length} chars in — integration pending`); - return { text: '[stub — ai-parasail integration not yet implemented]', model: 'PARASAIL_API_KEY' }; + id: "ai-parasail", + label: "Parasail", + defaultModel: "parasail-llama-33-70b-fp8", + models: ["parasail-llama-33-70b-fp8", "parasail-deepseek-r1", "parasail-qwen3-32b"], + + async generate(ctx, prompt, opts, config) { + const apiKey = ctx.secret("PARASAIL_API_KEY"); + if (!apiKey) throw new Error('PARASAIL_API_KEY not in vault - run `sh1pt promote ai setup`'); + const model = opts.model ?? "parasail-llama-33-70b-fp8"; + ctx.log(`ai-parasail · model=${model} · ${prompt.length} chars in`); + if (ctx.dryRun) return { text: '[dry-run]', model }; + + const messages: Array<{ role: string; content: string }> = []; + if (opts.system) messages.push({ role: 'system', content: opts.system }); + messages.push({ role: 'user', content: prompt }); + + const res = await fetch(chatCompletionsUrl(config.baseUrl ?? DEFAULT_BASE), { + method: 'POST', + headers: { + authorization: `Bearer ${apiKey}`, + 'content-type': 'application/json', + }, + body: JSON.stringify({ + model, + messages, + ...(opts.maxTokens !== undefined ? { max_completion_tokens: opts.maxTokens } : {}), + ...(opts.temperature !== undefined ? { temperature: opts.temperature } : {}), + ...opts.extra, + }), + }); + if (!res.ok) throw new Error(`Parasail ${res.status}: ${(await res.text()).slice(0, 200)}`); + const data = (await res.json()) as { + choices: Array<{ message?: { content?: string } }>; + model?: string; + usage?: { prompt_tokens?: number; completion_tokens?: number }; + }; + return { + text: data.choices[0]?.message?.content ?? '', + model: data.model ?? model, + inputTokens: data.usage?.prompt_tokens, + outputTokens: data.usage?.completion_tokens, + }; }, setup: tokenSetup({ - secretKey: 'https://parasail.io', - label: 'Parasail', - vendorDocUrl: '', - steps: [ - 'Sign in at and create an API key', - 'Copy the key — usually shown once', - 'Paste below; sh1pt encrypts it in the vault', + secretKey: "PARASAIL_API_KEY", + label: "Parasail", + vendorDocUrl: "https://docs.parasail.io/parasail-docs/cookbooks/chat-completions", + steps: ["Sign in at https://parasail.io and create an API key", "Copy the key - usually shown once", "Paste below; sh1pt encrypts it in the vault"], + fields: [ + { key: 'baseUrl', message: 'OpenAI-compatible base URL (optional; leave blank for the default):' }, ], }), }); diff --git a/packages/ai/venice/src/index.ts b/packages/ai/venice/src/index.ts index a0aa321f..5e60fdd1 100644 --- a/packages/ai/venice/src/index.ts +++ b/packages/ai/venice/src/index.ts @@ -4,27 +4,65 @@ interface Config { baseUrl?: string; } +const DEFAULT_BASE = "https://api.venice.ai/api"; + +function chatCompletionsUrl(baseUrl: string): string { + const base = baseUrl.replace(/\/+$/, ''); + return base.endsWith('/v1') ? `${base}/chat/completions` : `${base}/v1/chat/completions`; +} + export default defineAi({ - id: 'ai-venice', - label: 'Venice AI', - defaultModel: 'llama-3.3-70b', - models: ['llama-3.3-70b'], - - async generate(ctx, prompt, _opts, _config) { - const apiKey = ctx.secret('VENICE_API_KEY'); - if (!apiKey) throw new Error('VENICE_API_KEY not in vault — run `sh1pt promote ai setup`'); - ctx.log(`[stub] ai-venice · ${prompt.length} chars in — integration pending`); - return { text: '[stub — ai-venice integration not yet implemented]', model: 'llama-3.3-70b' }; + id: "ai-venice", + label: "Venice AI", + defaultModel: "llama-3.3-70b", + models: ["llama-3.3-70b", "qwen3-235b", "dolphin-2.9.2-qwen2-72b"], + + async generate(ctx, prompt, opts, config) { + const apiKey = ctx.secret("VENICE_API_KEY"); + if (!apiKey) throw new Error('VENICE_API_KEY not in vault - run `sh1pt promote ai setup`'); + const model = opts.model ?? "llama-3.3-70b"; + ctx.log(`ai-venice · model=${model} · ${prompt.length} chars in`); + if (ctx.dryRun) return { text: '[dry-run]', model }; + + const messages: Array<{ role: string; content: string }> = []; + if (opts.system) messages.push({ role: 'system', content: opts.system }); + messages.push({ role: 'user', content: prompt }); + + const res = await fetch(chatCompletionsUrl(config.baseUrl ?? DEFAULT_BASE), { + method: 'POST', + headers: { + authorization: `Bearer ${apiKey}`, + 'content-type': 'application/json', + }, + body: JSON.stringify({ + model, + messages, + ...(opts.maxTokens !== undefined ? { max_tokens: opts.maxTokens } : {}), + ...(opts.temperature !== undefined ? { temperature: opts.temperature } : {}), + ...opts.extra, + }), + }); + if (!res.ok) throw new Error(`Venice AI ${res.status}: ${(await res.text()).slice(0, 200)}`); + const data = (await res.json()) as { + choices: Array<{ message?: { content?: string } }>; + model?: string; + usage?: { prompt_tokens?: number; completion_tokens?: number }; + }; + return { + text: data.choices[0]?.message?.content ?? '', + model: data.model ?? model, + inputTokens: data.usage?.prompt_tokens, + outputTokens: data.usage?.completion_tokens, + }; }, setup: tokenSetup({ - secretKey: 'VENICE_API_KEY', - label: 'Venice AI', - vendorDocUrl: 'https://venice.ai', - steps: [ - 'Sign in at https://venice.ai and create an API key', - 'Copy the key — usually shown once', - 'Paste below; sh1pt encrypts it in the vault', + secretKey: "VENICE_API_KEY", + label: "Venice AI", + vendorDocUrl: "https://docs.venice.ai/api-reference/api-spec", + steps: ["Sign in at https://venice.ai and create an API key", "Copy the key - usually shown once", "Paste below; sh1pt encrypts it in the vault"], + fields: [ + { key: 'baseUrl', message: 'OpenAI-compatible base URL (optional; leave blank for the default):' }, ], }), }); diff --git a/packages/core/src/setup-helpers.ts b/packages/core/src/setup-helpers.ts index 09aea2b4..d53a55b6 100644 --- a/packages/core/src/setup-helpers.ts +++ b/packages/core/src/setup-helpers.ts @@ -196,6 +196,8 @@ export function tokenSetup(opts: TokenSetupOpts): SetupFn { if (val) { if (field.secret) await ctx.setSecret(field.key, val); else configExtras[field.key] = val; + } else if (!field.secret) { + configExtras[field.key] = ''; } } diff --git a/packages/core/src/setup.ts b/packages/core/src/setup.ts index 9a465491..ecb04967 100644 --- a/packages/core/src/setup.ts +++ b/packages/core/src/setup.ts @@ -16,7 +16,7 @@ // // Secrets go to the vault via ctx.setSecret — never into config.json. -import { configPath, setAdapterConfig } from './config-store.js'; +import { configPath, getAdapterConfig, setAdapterConfig } from './config-store.js'; export interface SetupPromptDef { type: 'text' | 'password' | 'select' | 'confirm'; @@ -78,7 +78,9 @@ export async function runSetup( }; } - await setAdapterConfig(adapter.id, result.config); + const existingConfig = await getAdapterConfig(adapter.id); + const configToSave = mergeAdapterConfig(existingConfig, result.config); + await setAdapterConfig(adapter.id, configToSave); ctx.log(` saved → ${configPath()} · adapters.${adapter.id}`); if (!result.ok && result.manual && result.manual.length > 0) { @@ -91,3 +93,18 @@ export async function runSetup( return result; } + + +function isPlainRecord(value: unknown): value is Record { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function mergeAdapterConfig(existing: unknown, next: unknown): unknown { + if (!isPlainRecord(existing) || !isPlainRecord(next)) return next; + const merged: Record = { ...existing }; + for (const [key, value] of Object.entries(next)) { + if (value === '') delete merged[key]; + else merged[key] = value; + } + return merged; +}