Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Mistral OpenRouter API #113

@alexisbohns

Description

@alexisbohns

Résumer un “event” (avec ses responses) via Mistral Small 3.2 24B (free) sur OpenRouter depuis SvelteKit, avec une option Supabase si tu veux déporter l’appel.

  1. Créer et sécuriser la clé OpenRouter
    1. Crée un compte sur openrouter.ai puis crée une API key depuis le dashboard (tu peux aussi les créer via API). L’API utilise un header Authorization: Bearer .... 
    2. L’endpoint à utiliser est POST https://openrouter.ai/api/v1/chat/completions. 
    3. Modèle: mistralai/mistral-small-3.2-24b-instruct:free (page modèle + alias exact). 
    4. (Optionnel mais recommandé) ajoute les headers HTTP-Referer (URL de ton app) et X-Title (nom de l’app) pour l’attribution/rankings. 

Stocke la clé côté serveur (SvelteKit .env) — ne l’expose jamais au client.

  1. Variables d’environnement (SvelteKit)

.env (ou .env.local)

OPENROUTER_API_KEY=xxxxxxxxxxxxxxxx
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
OPENROUTER_MODEL=mistralai/mistral-small-3.2-24b-instruct:free
APP_URL=https://pebbles.yourdomain.com
APP_NAME=Pebbles

  1. Client SvelteKit côté serveur : un petit SDK maison

src/lib/server/openrouter.ts

// Server-only module
import { env } from '$env/dynamic/private';

type ChatMessage = { role: 'system' | 'user' | 'assistant'; content: string };

export async function chatCompletion(messages: ChatMessage[], opts?: {
  max_tokens?: number;
  temperature?: number;
  response_format?: any; // si tu veux du JSON mode "structured output" plus tard
}) {
  const res = await fetch(`${env.OPENROUTER_BASE_URL}/chat/completions`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${env.OPENROUTER_API_KEY}`,
      'Content-Type': 'application/json',
      'HTTP-Referer': env.APP_URL ?? 'http://localhost:5173',
      'X-Title': env.APP_NAME ?? 'Pebbles'
    },
    body: JSON.stringify({
      model: env.OPENROUTER_MODEL,
      messages,
      temperature: opts?.temperature ?? 0.3,
      max_tokens: opts?.max_tokens ?? 600
    })
  });

  if (!res.ok) {
    const err = await res.text();
    throw new Error(`OpenRouter error ${res.status}: ${err}`);
  }

  const data = await res.json();
  return data.choices?.[0]?.message?.content ?? '';
}

Référence format & endpoint: /api/v1/chat/completions. 

  1. Construire le prompt de résumé à partir de Supabase

(A) Option simple 100% SvelteKit
• Tu sélectionnes l’event + ses responses dans un endpoint serveur SvelteKit, tu construis le prompt, tu appelles chatCompletion(), puis tu sauvegardes le résumé.

Schéma Supabase (tables) déjà présents dans ton projet (events, responses, emotions, …). 

src/routes/api/summarize/[event_id]/+server.ts

import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { createClient } from '@supabase/supabase-js';
import { chatCompletion } from '$lib/server/openrouter';
import { env } from '$env/dynamic/private';

export const POST: RequestHandler = async ({ params, locals }) => {
  const event_id = params.event_id;

  const supabase = createClient(env.PUBLIC_SUPABASE_URL!, env.SUPABASE_SERVICE_ROLE_KEY!);
  // service_role côté serveur uniquement

  // 1) Récupérer event + responses (situation, émotions, pensées, etc.)
  const { data: event, error: e1 } = await supabase
    .from('events')
    .select('*')
    .eq('id', event_id)
    .single();
  if (e1 || !event) return new Response('Event not found', { status: 404 });

  const { data: responses } = await supabase
    .from('responses')
    .select('question_id, value')
    .eq('event_id', event_id);

  // (optionnel) récupérer les émotions mappées
  const { data: emotions } = await supabase
    .from('emotion_mapping')
    .select('emotion_id, valence, emotions(name)')
    .eq('event_id', event_id)
    .returns<any[]>();

  // 2) Construire le prompt
  const userBlock = [
    `Event: ${event.name ?? '(sans titre)'}${event.occurrence_date}`,
    `Valence: ${event.valence ?? 'n/a'}`,
    `Responses:`,
    ...(responses ?? []).map((r) => `- ${r.question_id}: ${r.value}`),
    emotions?.length
      ? `Emotions:\n${emotions.map((m) => `- ${m.emotions?.name}: ${m.valence}`).join('\n')}`
      : ''
  ].join('\n');

  const system = `You are Pebbles' cognitive journaling summarizer.
Create a concise, neutral, psychologically-informed summary of the user's entry.
Output in French, markdown, <= 120 words, with 3 bullet highlights and 1 actionable reframe.
Avoid clinical jargon.`;

  // 3) Appel OpenRouter
  const summary = await chatCompletion([
    { role: 'system', content: system },
    { role: 'user', content: userBlock }
  ]);

  // 4) Sauvegarder le résumé (table à créer ci-dessous)
  const { error: e2 } = await supabase
    .from('summaries')
    .insert({ event_id, summary_text: summary });

  if (e2) return new Response(`Save error: ${e2.message}`, { status: 500 });

  return json({ summary });
};

(B) Petite table pour stocker les résumés

create table if not exists public.summaries (
  id uuid primary key default gen_random_uuid(),
  event_id uuid not null references public.events(id) on delete cascade,
  summary_text text not null,
  created_at timestamptz default now()
);

Les FK/colonnes event/responses/… correspondent à ton schéma Pebbles. 

  1. (Option) RPC Supabase pour packager les données d’un event

But : réduire les allers-retours et normaliser l’input du LLM.

create or replace function public.get_event_bundle(p_event_id uuid)
returns jsonb
language sql
security definer
stable
as $$
select jsonb_build_object(
  'event', to_jsonb(e),
  'responses', coalesce(
    (select jsonb_agg(r) from public.responses r where r.event_id = e.id), '[]'::jsonb
  ),
  'emotions', coalesce(
    (select jsonb_agg(jsonb_build_object(
      'name', em.name, 'valence', m.valence
    ))
     from public.emotion_mapping m
     join public.emotions em on em.id = m.emotion_id
     where m.event_id = e.id), '[]'::jsonb
  )
)
from public.events e
where e.id = p_event_id;
$$;

Dans SvelteKit tu fais :

const { data: bundle } = await supabase.rpc('get_event_bundle', { p_event_id: event_id });

…et tu formates le userBlock à partir de bundle.

  1. (Option) Supabase Edge Function pour appeler OpenRouter

Si tu veux pouvoir déclencher depuis le client (mobile, CRON) sans exposer la clé au navigateur, crée une Edge Function (Deno) qui appelle OpenRouter:

supabase/functions/summarize/index.ts

// deno-lint-ignore-file no-explicit-any
import 'jsr:@supabase/functions-js/edge-runtime.d.ts';

Deno.serve(async (req) => {
  const { event_id, bundle } = await req.json();

  // build prompt à partir de bundle (ou fetch depuis DB avec service role si besoin)
  const messages = [
    { role: 'system', content: 'You are Pebbles summarizer in French...' },
    { role: 'user', content: JSON.stringify(bundle) }
  ];

  const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${Deno.env.get('OPENROUTER_API_KEY')}`,
      'Content-Type': 'application/json',
      'HTTP-Referer': Deno.env.get('APP_URL')!,
      'X-Title': Deno.env.get('APP_NAME')!
    },
    body: JSON.stringify({
      model: 'mistralai/mistral-small-3.2-24b-instruct:free',
      messages,
      temperature: 0.3,
      max_tokens: 600
    })
  });

  if (!res.ok) return new Response(await res.text(), { status: res.status });

  const data = await res.json();
  const summary = data.choices?.[0]?.message?.content ?? '';

  // (option) écrire dans public.summaries ici…

  return new Response(JSON.stringify({ summary }), { headers: { 'Content-Type': 'application/json' }});
});

Auth & headers OpenRouter restent identiques. 

  1. Prompt design (prêt pour Pebbles)

Tu peux stabiliser la sortie avec un mini “schema” markdown :

RÉSUMÉ (≤120 mots)

- Insight 1
- Insight 2
- Insight 3

🧭 Re-cadrage : …

Ou demander du JSON si tu préfères un rendu structuré (et re-render côté app) :

const system = `Return strict JSON: {"summary": "...","insights":["...","...","..."],"reframe":"..."}`;

(Mistral Small 3.2 gère bien le suivi d’instructions et le structured output.) 

  1. Streaming (facultatif)

Si tu veux afficher la génération en temps réel, utilise l’API streaming du même endpoint (SSE). La doc OpenRouter précise que le chat completions supporte streaming et non-streaming. 
Tu peux t’inspirer d’exemples SvelteKit “API route → stream au client”. (Réf. article API SvelteKit pour la structure générale des routes.) 

  1. Récap étapes concrètes
  1. Créer la clé OpenRouter (dashboard) → copie dans .env. 
  2. Installer Supabase JS côté serveur si ce n’est pas déjà fait.
  3. Coder src/lib/server/openrouter.ts (wrapper). 
  4. Créer l’endpoint POST /api/summarize/[event_id] qui :
  • lit l’event + responses,
  • construit le prompt,
  • appelle OpenRouter,
  • écrit en DB (table summaries).
  1. (Option) RPC get_event_bundle pour un payload propre.
  2. (Option) Edge Function Supabase si tu veux un point d’entrée sécurisé depuis le client.
  3. Affichage UI : appelle l’endpoint et stocke le résultat dans ta timeline/analysis.

Metadata

Metadata

Assignees

Labels

featureNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions