Files
spoon/packages/backend/convex/aiProviderProfilesNode.ts
T
2026-06-22 10:37:26 -04:00

91 lines
2.6 KiB
TypeScript

'use node';
import { getAuthUserId } from '@convex-dev/auth/server';
import { ConvexError, v } from 'convex/values';
import type { Id } from './_generated/dataModel';
import type { ActionCtx } from './_generated/server';
import { internal } from './_generated/api';
import { action } from './_generated/server';
import { encryptSecret } from './secretCrypto';
const provider = v.union(
v.literal('openai'),
v.literal('anthropic'),
v.literal('google'),
v.literal('openrouter'),
v.literal('requesty'),
v.literal('litellm'),
v.literal('cloudflare_ai_gateway'),
v.literal('custom_openai_compatible'),
v.literal('opencode_openai_login'),
);
const authType = v.union(
v.literal('api_key'),
v.literal('opencode_auth_json'),
v.literal('none'),
);
const reasoningEffort = v.union(
v.literal('none'),
v.literal('minimal'),
v.literal('low'),
v.literal('medium'),
v.literal('high'),
v.literal('xhigh'),
);
const getRequiredUserId = async (ctx: ActionCtx): Promise<Id<'users'>> => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new ConvexError('Not authenticated.');
return userId;
};
const previewSecret = (secret: string) => {
const trimmed = secret.trim();
if (!trimmed) return undefined;
if (trimmed.startsWith('{')) return 'auth json configured';
if (trimmed.length <= 10) return 'configured';
return `${trimmed.slice(0, 7)}...${trimmed.slice(-4)}`;
};
export const save = action({
args: {
profileId: v.optional(v.id('aiProviderProfiles')),
name: v.string(),
provider,
authType,
secret: v.optional(v.string()),
baseUrl: v.optional(v.string()),
defaultModel: v.string(),
modelOptions: v.optional(v.array(v.string())),
reasoningEffort,
enabled: v.boolean(),
},
handler: async (ctx, args): Promise<Id<'aiProviderProfiles'>> => {
const ownerId = await getRequiredUserId(ctx);
const secret = args.secret?.trim();
if (!args.profileId && args.authType !== 'none' && !secret) {
throw new ConvexError('A credential is required for this provider.');
}
return await ctx.runMutation(
internal.aiProviderProfiles.upsertEncryptedInternal,
{
ownerId,
profileId: args.profileId,
name: args.name,
provider: args.provider,
authType: args.authType,
encryptedSecret: secret ? encryptSecret(secret) : undefined,
secretPreview: secret ? previewSecret(secret) : undefined,
baseUrl: args.baseUrl,
defaultModel: args.defaultModel,
modelOptions: args.modelOptions,
reasoningEffort: args.reasoningEffort,
enabled: args.enabled,
},
);
},
});