Aller au contenu

cortex_lab

Skill de référence : .claude/skills/cortex-compat/SKILL.md. Contrats vérifiés : .claude/skills/cortex-compat/references/contracts.md.

But : le même code appelle les fonctions AI_* au labo et chez le client. Au labo, elles passent par cortex_lab puis LiteLLM ; chez Snowflake, elles s'exécutent nativement. Les réponses diffèrent, les contrats (noms, signatures, formes de retour, erreurs, limites) sont identiques. Cible : DuckDB et Python uniquement ; Trino n'est pas ciblé (ses fonctions IA ont d'autres noms).

Fonctions couvertes

Fonction Contrat Alias LiteLLM Façade SQL DuckDB Snowpark (cortex_lab.snowpark) snowflake.cortex (cortex_lab.cortex)
AI_CLASSIFY vérifié le 2026-10-22 cortex-classify macro ai_classify, 2 à 4 arguments ai_classify classify_text
AI_SENTIMENT vérifié le 2026-10-22 cortex-sentiment macro ai_sentiment, 1 à 3 arguments ai_sentiment sentiment (score float)
AI_COMPLETE (forme chaîne) vérifié le 2026-10-22, partiellement nom de modèle passé en argument (cortex-complete au labo) macro ai_complete, 2 à 6 arguments ai_complete complete
GET page GET vérifiée le 2026-10-22 aucun macro get get aucun
PROMPT vérifié le 2026-10-23 aucun macro prompt, 2 à 11 arguments (le gabarit et 1 à 10 expressions) prompt aucun
AI_FILTER (texte et PROMPT) vérifié le 2026-10-23 cortex-filter macro ai_filter, 1 à 2 arguments ai_filter aucun
AI_EXTRACT (texte) vérifié le 2026-10-23 cortex-extract macro ai_extract(text, responseFormat, scores := NULL), arguments nommés admis ai_extract aucun
AI_TRANSLATE vérifié le 2026-10-23 cortex-translate macro ai_translate, 3 à 4 arguments ai_translate translate
AI_SUMMARIZE (texte) vérifié le 2026-10-25 cortex-summarize macro ai_summarize(text, return_error_details := NULL) aucun (absente de la référence Snowpark) summarize
AI_AGG vérifié le 2026-10-23 cortex-agg macro d'agrégat ai_agg(expr, instruction) ai_agg aucun
AI_SUMMARIZE_AGG vérifié le 2026-10-23 cortex-summarize-agg macro d'agrégat ai_summarize_agg(expr) ai_summarize_agg aucun
AI_EMBED (texte) vérifié le 2026-10-23 cortex-embed (bge-m3, 1 024 dimensions) macro ai_embed, 2 à 3 arguments ai_embed embed_text_1024 (embed_text_768 refusée)
AI_SIMILARITY (texte) vérifié le 2026-10-23 cortex-embed macro ai_similarity, 2 à 4 arguments ai_similarity aucun
AI_COUNT_TOKENS vérifié le 2026-10-23 aucun (estimation) macro ai_count_tokens, 2 à 5 arguments ai_count_tokens aucun
AI_REDACT vérifié le 2026-10-23 cortex-redact macro ai_redact(input, categories := NULL, return_error_details := NULL, mode := NULL) ai_redact aucun

Les alias de chat pointent vers ollama_chat/qwen3:8b et l'alias cortex-embed vers ollama/bge-m3-cpu sur la brique LiteLLM du NUC, sans repli vers un modèle cloud (voir exploitation.md). La clé du labo est limitée à ces alias cortex-* : un appel sur claude-sonnet-4-6 est refusé (401), test d'intégration. Toute autre fonction AI_* passe par /new-cortex-function. Les fonctions sur fichiers (TO_FILE, AI_PARSE_DOCUMENT, AI_EXTRACT fichier, AI_FILTER image, AI_SUMMARIZE fichier) et les formes image sont exclues (décision A1 du 2026-10-23). Le détail de chaque contrat et ses écarts de fidélité sont dans contracts.md.

Organisation du module

cortex_lab/
  __init__.py
  core/
    client.py        client LiteLLM (SDK openai pointé sur LITELLM_BASE_URL), chat et embeddings, interface injectable, garde de fenêtre
    errors.py        erreurs par ligne, enveloppe {"value": ..., "error": ...}
    limits.py        limites documentées, fenêtre du modèle local
    routing.py       alias LiteLLM par fonction, alias de modèle pour AI_COMPLETE, noms de modèles d'embedding
    functions/
      ai_classify.py ai_sentiment.py ai_complete.py   prompt, schéma Pydantic ou JSON, validation
      ai_filter.py ai_extract.py ai_translate.py ai_summarize.py ai_agg.py ai_summarize_agg.py
      ai_embed.py ai_similarity.py ai_redact.py ai_count_tokens.py prompt.py
  duckdb/
    __init__.py      register(con) : UDF internes et macros publiques
    macros.sql       macros aux noms Cortex et macro get
  snowpark.py        fonctions ai_* style Snowpark (expressions de colonnes DuckDB)
  cortex.py          fonctions style snowflake.cortex (appels directs du cœur)
  compat.py          bascule à l'import selon CORTEX_BACKEND
  dbt_plugin.py      plugin dbt-duckdb : configure_connection appelle register

Toute la logique vit dans core/. Les façades n'adaptent que les arguments.

Contrats

AI_CLASSIFY

  • Signature : AI_CLASSIFY(input, list_of_categories [, config_object] [, return_error_details]).
  • Catégories : au moins 2 valeurs uniques, sensibles à la casse ; chaînes, ou objets {'label': ..., 'description': ...} (description de 25 mots maximum). La page SQL de Snowflake ne fixe aucun maximum pour une entrée texte (limite de la fenêtre de jetons, précision moindre au-delà de vingt catégories) ; le maximum de 100 vient du docstring Snowpark et de l'entrée document. Le code du labo refuse au-delà de 100 : écart nouveau, à traiter par avenant (relecture du 2026-10-24).
  • config_object : task_description (50 mots maximum), output_mode (single par défaut ou multi), examples (objets input, labels, explanation).
  • Retour : {"labels": [...]}, exactement un label en mode single, un ou plusieurs en mode multi.
  • Référence : https://docs.snowflake.com/en/sql-reference/functions/ai_classify (lue le 2026-10-22) ; Snowpark : https://docs.snowflake.com/en/developer-guide/snowpark/reference/python/latest/snowpark/api/snowflake.snowpark.functions.ai_classify (lue le 2026-10-22).

AI_SENTIMENT

  • Signature : AI_SENTIMENT(text [, categories] [, return_error_details]).
  • Catégories : 10 au maximum, 30 caractères chacune. Fenêtre de contexte : 2 048 jetons (estimés au labo à 4 caractères par jeton).
  • Retour : {"categories": [{"name": ..., "sentiment": ...}, ...]}, entrée overall toujours présente et en premier ; sentiment dans unknown, positive, negative, neutral, mixed.
  • Référence : https://docs.snowflake.com/en/sql-reference/functions/ai_sentiment (lue le 2026-10-22).

AI_COMPLETE (forme chaîne)

  • Signature : AI_COMPLETE(model, prompt [, model_parameters, response_format, show_details] [, return_error_details]).
  • model_parameters : temperature et top_p entre 0 et 1, max_tokens (défaut 4096, plafond 8192 tiré du seul docstring Snowpark : la page SQL ne donne aucun plafond, relue le 2026-10-23), guardrails (TRUE est une erreur au labo : Cortex Guard non reproduit).
  • response_format : {'type': 'json', 'schema': {...}}, schéma validé par jsonschema (Draft 2020-12), sortie validée contre le schéma.
  • Retour : chaîne ; objet conforme au format avec response_format ; objet {"choices", "created", "model", "usage"} avec show_details ; objet {"structured_output": [{"raw_message", "type"}], "created", "model", "usage"} avec les deux.
  • Non reproduit : formes image et objet prompt, littéral TYPE OBJECT(...), modèle apporté par le client. Un return_error_details TRUE sur une entrée NULL renvoie NULL (comportement Snowflake non documenté).
  • Références : https://docs.snowflake.com/en/sql-reference/functions/ai_complete, https://docs.snowflake.com/en/sql-reference/functions/ai_complete-single-string, https://docs.snowflake.com/en/sql-reference/functions/ai_complete-structured-outputs (lues le 2026-10-22, la dernière en partie). Pages des formes image et objet prompt lues le 2026-10-23 (non reproduites) ; reste à vérifier chez le client : limites de taille par modèle Snowflake. Note BCR-2184 lue : l'erreur passe à NULL pour AI_COMPLETE, AI_CLASSIFY, AI_FILTER, AI_SENTIMENT, AI_TRANSLATE, AI_COUNT_TOKENS.

Fonctions de l'avenant du 2026-10-23

Pages lues le 2026-10-23 (URL dans contracts.md). Résumé des contrats et des écarts ; le détail est dans contracts.md.

Fonction Retour Erreurs Écarts principaux
PROMPT(gabarit, e1 [, ... e10]) objet {"template", "args"} ; ne formate rien repère sans expression : erreur 10 expressions au plus (A9) ; args convertis en texte ; type JSON
AI_FILTER(input [, rd]) BOOLEAN NULL ; {"value", "error"} avec rd forme fichier refusée ; avec rd (même FALSE) la macro renvoie JSON
AI_EXTRACT(text, responseFormat) {"error": null, "response": {...}} erreur levée (A7) forme fichier et config non reproduites ; scores => TRUE refusé (les scores sont GA chez Snowflake depuis le 2026-05-22) ; argument model => (modèle affiné, forme fichier) non reproduit ; limites de 100 entités et 10 tables
AI_TRANSLATE(text, source, cible [, rd]) VARCHAR NULL ; objet avec rd 23 codes ; limite effective de la fenêtre locale
AI_SUMMARIZE(text [, rd]) (texte) VARCHAR NULL ; objet avec rd (inféré de BCR-2184, la page ne détaille rien) entrée de 110 caractères ou moins renvoyée telle quelle sans appel, et entrée renvoyée si le résumé est plus long ; forme fichier refusée ; limite effective de la fenêtre locale
AI_AGG(expr, instruction), AI_SUMMARIZE_AGG(expr) VARCHAR erreur levée (A7) découpe et réduction par étapes au-delà de la fenêtre ; pas de clause OVER
AI_EMBED(model, input [, rd]) vecteur de 1 024 flottants NULL ; objet avec rd (BCR-2358, activé par défaut dans le bundle 2026_07 depuis la version 10.37) noms 768, image et non documentés refusés (snowflake-arctic-embed-l-v2.0-8k) ; multilingual-e5-large accepté (1 024 dimensions écrites par la page d'EMBED_TEXT_1024, servi par bge-m3, décision S2) ; FLOAT[1024] au lieu de VECTOR
AI_SIMILARITY(a, b [, config] [, rd]) FLOAT de -1 à 1 NULL ; objet avec rd (BCR-2358) texte seulement ; rd demande la position de la configuration (NULL admis)
AI_REDACT(input [, categories] [, rd] [, mode]) VARCHAR, ou {"spans": [...]} en mode detect erreur levée sauf rd TRUE (A7) 13 catégories ; le labo calcule les positions et la substitution ; pas un contrôle de sécurité
AI_COUNT_TOKENS(fonction, [modèle,] texte [, options] [, rd]) entier (VARCHAR en SQL) NULL ; objet avec rd estimation à 4 caractères par jeton (A8) ; chez Snowflake, compte exact pour la plupart des modèles, estimation à moins de 3 % pour Claude et Gemini, quasi exact pour OpenAI

rd désigne return_error_details. Les erreurs levées (A7) sont celles que la note BCR-2184 de Snowflake ne fait pas passer à NULL : AI_EXTRACT, AI_AGG, AI_SUMMARIZE_AGG, AI_REDACT. Dans DuckDB, l'exception fait échouer la requête (exception_handling="default" pour ces UDF).

Fenêtre de contexte du modèle local

Le modèle local coupe en silence un prompt qui ne tient pas dans sa fenêtre (mesure du 2026-10-23, test B11) : 32 768 jetons pour les alias de chat, 2 048 pour cortex-embed (la passerelle en annonce 8 192). Le labo refuse le prompt avant l'appel : un texte plus long que la fenêtre donne une erreur par ligne (NULL, ou l'erreur levée pour les fonctions de l'écart A7), jamais une troncature. La règle est pessimiste (2 caractères ASCII par jeton, un jeton par autre caractère) et s'applique à toutes les fonctions de chat, y compris AI_CLASSIFY, AI_SENTIMENT et AI_COMPLETE. Elle refuse donc des textes que le modèle accepterait. Pour les embeddings, un contrôle après l'appel détecte aussi une coupure (jetons rapportés égaux à la fenêtre). AI_AGG et AI_SUMMARIZE_AGG découpent la liste en tranches qui tiennent dans la fenêtre puis réduisent les réponses partielles par étapes.

Les modèles locaux raisonnent : leurs jetons de réflexion comptent dans max_tokens et laissaient une réponse vide. Le client envoie donc think: false (commutateur d'Ollama, transmis par la passerelle).

Règles communes

  • Forme de retour identique à Snowflake, garantie par une sortie structurée envoyée à LiteLLM (response_format de type json_schema, strict: true, avec énumérations : labels fournis, valeurs de sentiment, noms de catégories) puis validée par Pydantic (ClassifyOutput, SentimentOutput) ou jsonschema. Une sortie invalide (label hors liste, JSON mal formé, sentiment hors énumération, catégorie manquante ou répétée) est une erreur, jamais un résultat.
  • Erreurs : une ligne en erreur renvoie NULL sans bloquer la requête (sauf AI_EXTRACT, AI_AGG, AI_SUMMARIZE_AGG et AI_REDACT, dont l'erreur est levée). Avec return_error_details à TRUE, la fonction renvoie {"value": ..., "error": ...}, exactement un des deux non NULL (cortex_lab.core.errors.run_row). Une entrée NULL donne NULL, sans erreur.
  • Hiérarchie d'erreurs : CortexError, et ses sous-classes InvalidArgumentError, LimitExceededError, InvalidModelOutputError, UpstreamError. Les messages ne contiennent jamais le contenu de la requête ni la configuration.
  • Limites reproduites dans cortex_lab/core/limits.py pour échouer au labo comme en production.
  • Température 0 par défaut pour toutes les fonctions.

SQL à deux étages (DuckDB)

  1. UDF internes _ai_classify (4 arguments), _ai_sentiment (3), _ai_complete (6), _ai_filter (2), _ai_extract (3), _ai_translate (4), _ai_summarize (2), _ai_embed (2, retour FLOAT[1024]), _ai_embed_details (3), _ai_similarity (3, retour DOUBLE), _ai_similarity_details (4), _ai_agg (2, premier argument VARCHAR[]), _ai_summarize_agg (1, argument VARCHAR[]), _ai_redact (4), _ai_count_tokens (5) : arguments VARCHAR, les arguments structurés ou optionnels voyagent en texte JSON. Enregistrées par cortex_lab.duckdb.register(con) avec type="arrow" (lots de lignes, appels LLM concurrents jusqu'à 8 fils), null_handling="special" (un argument optionnel NULL atteint la fonction), exception_handling="return_null" (filet de dernier recours ; "default" pour les UDF dont l'erreur est levée) et side_effects=True (non déterministe, pas de réutilisation de résultat). Les erreurs sont traitées ligne par ligne : une exception ne peut pas annuler tout le lot.
  2. Macros publiques aux noms Cortex (cortex_lab/duckdb/macros.sql), une surcharge par nombre d'arguments, qui convertissent avec to_json() puis castent en JSON (ai_classify, ai_sentiment). ai_complete renvoie toujours un VARCHAR (chaîne, ou texte JSON d'un objet à lire avec GET).

Quand une position peut recevoir soit un objet, soit le booléen return_error_details, c'est le cœur Python qui tranche selon le type JSON reçu (split_classify_args, split_sentiment_args, split_complete_args, split_redact_args).

Les macros d'agrégat ai_agg et ai_summarize_agg enveloppent list(...) : l'UDF reçoit tout le groupe (GROUP BY ou constante). Les macros ai_extract, ai_redact et ai_summarize déclarent les noms de paramètres de Snowflake, donc text =>, responseFormat =>, categories =>, mode => fonctionnent (DuckDB 2.0.0 accepte => et := pour les paramètres nommés d'une macro, sans tenir compte de la casse, test B13). Une macro n'a qu'un type de retour par surcharge : ai_filter renvoie un BOOLEAN à un argument et un JSON avec return_error_details, ai_embed un FLOAT[1024] puis un JSON, ai_similarity un DOUBLE puis un JSON, ai_translate, ai_summarize, ai_redact et ai_count_tokens toujours un VARCHAR.

Macro get(x, field) : field chaîne (clé) ou entier (indice). Renvoie NULL pour une clé absente, une chaîne JSON sans guillemets, et le texte JSON pour un objet, un tableau ou un nombre. Elle remplace la syntaxe Snowflake x:champ, absente de DuckDB. Dans dbt, la macro get du projet dispatche selon l'adapter (voir collecte-retraitement.md).

Exemple, identique dans DuckDB au labo et dans Snowflake :

SELECT GET(AI_CLASSIFY(review, ['room', 'service', 'price']), 'labels') AS labels
FROM reviews;

Les littéraux ['a', 'b'] et {'cle': 'valeur'} s'écrivent de la même façon dans DuckDB et dans Snowflake.

Routage vers LiteLLM

  • cortex_lab/core/routing.py : ALIASES associe chaque fonction à modèle géré par Snowflake, sans argument de modèle, à son alias (ai_classify à cortex-classify, ai_filter à cortex-filter, ai_extract à cortex-extract, ai_translate à cortex-translate, ai_summarize à cortex-summarize, ai_agg à cortex-agg, ai_summarize_agg à cortex-summarize-agg, ai_redact à cortex-redact, ai_sentiment à cortex-sentiment). Pour AI_EMBED et AI_SIMILARITY, embed_alias associe les noms de 1 024 dimensions (snowflake-arctic-embed-l-v2.0, voyage-multilingual-2, nv-embed-qa-4, multilingual-e5-large) à l'alias cortex-embed et refuse les autres (768 dimensions, image, dimension non documentée, inconnus) : le nom d'un modèle Snowflake n'est jamais un alias de la brique, qui porte des alias adossés au cloud sous certains de ces noms. Pour AI_COMPLETE, le nom de modèle passé en argument est utilisé comme alias LiteLLM du même nom (alias_for_complete, motif ^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$), mais seulement s'il figure dans la liste d'alias autorisés : les alias cortex-* connus (ALIASES, cortex-embed, cortex-complete) plus ceux de la variable CORTEX_COMPLETE_EXTRA_ALIASES (noms séparés par des virgules, lue à chaque appel, noms invalides ignorés). Un nom refusé (par exemple claude-sonnet-4-6, alias adossé au cloud sur la brique) donne l'erreur du contrat, NULL ou return_error_details, sans appel réseau. C'est une seconde barrière : la première reste le périmètre de la clé snowflake-lab-kit.
  • cortex_lab/core/client.py : LiteLLMClient construit un client openai.OpenAI avec base_url lu dans LITELLM_BASE_URL (jamais de valeur par défaut, jamais en dur), clé dans LITELLM_API_KEY, délai LITELLM_TIMEOUT_SECONDS (défaut 120 secondes), deux nouvelles tentatives. get_client() crée un client par processus ; set_client() permet d'injecter un faux client (tests).
  • Aucune dépendance litellm dans le dépôt : les versions 1.82.7 et 1.82.8 (vol d'identifiants) sont de toute façon bloquées par un hook et par un test.

Façades Python

cortex_lab.snowpark

Signatures de snowflake.snowpark.functions (snowflake-snowpark-python 1.55.3, lues dans le code du paquet le 2026-10-22) : ai_classify(expr, list_of_categories, return_error_details=None, **kwargs) (task_description, output_mode, examples en kwargs), ai_sentiment(text, categories=None, return_error_details=None), ai_complete(model, prompt, file=None, model_parameters=None, response_format=None, show_details=None, return_error_details=None), plus get(expr, field).

Chaque fonction renvoie une duckdb.FunctionExpression qui appelle la macro du même nom ; une chaîne est un littéral, une expression DuckDB est utilisée telle quelle ; les arguments None de fin sont retirés. L'indexation de colonne Snowpark (col["labels"][0]) n'existe pas sur les expressions DuckDB : utiliser get(expr, "labels"). file (forme image) lève NotImplementedError.

Signatures des fonctions de l'avenant (lues dans le même paquet le 2026-10-23) : prompt(template_string, *exprs) (une chaîne parmi exprs est un nom de colonne), ai_filter(predicate, file=None, return_error_details=None), ai_extract(input, response_format, scores=None, config=None) (scores vrai et config lèvent NotImplementedError), ai_summarize_agg(expr), ai_agg(expr, task_description), ai_similarity(input1, input2, **kwargs) (clé model), ai_embed(model, input), ai_translate(text, source_language, target_language, return_error_details=None), ai_count_tokens(function_name, input_text, model=None, options=None, return_error_details=None) et ai_redact(input, categories=None, mode=None, return_error_details=None). Pour ai_count_tokens et ai_redact, la façade remet les arguments dans l'ordre de la syntaxe SQL. Les fonctions d'agrégat s'utilisent dans rel.aggregate(...).

cortex_lab.cortex

Signatures de snowflake.cortex (snowflake-ml-python 2.6.0, lues dans le code du paquet le 2026-10-22 ; la page de documentation renvoyait 404) :

Fonction Retour Différences au labo
complete(model, prompt, *, options=None, session=None, stream=False, timeout=None, deadline=None) str (objet structuré sérialisé en JSON) stream=True et prompts de conversation refusés (NotImplementedError) ; session, timeout, deadline acceptés et ignorés
sentiment(text, session=None) float entre -1 et 1 Comme dans le paquet Snowflake, le score vient d'un appel AI_COMPLETE avec une consigne fixe et un schéma {"score": number} ; au labo l'appel passe par l'alias cortex-sentiment
classify_text(str_input, categories, session=None) chaîne JSON {"label": ...} Construite sur AI_CLASSIFY en mode single
translate(text, from_language, to_language, session=None) str Construite sur AI_TRANSLATE ; langue source '' pour la détection
summarize(text, session=None) str Construite sur AI_SUMMARIZE (forme texte) ; la page Snowflake donne « a column of string summaries », la forme str est une hypothèse ; entrée de 110 caractères ou moins renvoyée telle quelle
embed_text_1024(model, text, session=None) list[float] Construite sur AI_EMBED ; noms de 1 024 dimensions seulement
embed_text_768(model, text, session=None) list[float] Même signature, refuse toujours : aucun modèle de 768 dimensions derrière LiteLLM

Ces fonctions acceptent un argument nommé client pour l'injection en test. Les erreurs lèvent une exception au lieu de renvoyer NULL. Non reproduit : extract_answer.

État chez Snowflake au 2026-10-24 : les noms CamelCase de snowflake.ml.cortex sont dépréciés au profit des noms snake_case, que seul le labo expose. BCR-2432 (bundle 2026_08, désactivé par défaut) fait échouer les fonctions SQL historiques SNOWFLAKE.CORTEX dans les comptes inactifs, sans redirection ; le module Python reste valable avec snowflake-ml-python 2.4.0 ou plus, dont le backend appelle les fonctions AI_*. SUMMARIZE a pour remplaçant AI_SUMMARIZE (reproduite au labo depuis le 2026-10-25), EXTRACT_ANSWER a pour remplaçant AI_EXTRACT.

Bascule CORTEX_BACKEND

cortex_lab/compat.py choisit le module à l'import selon CORTEX_BACKEND :

Valeur complete, sentiment, classify_text, translate, summarize, embed_text_768, embed_text_1024 snowpark_functions
lab (défaut) cortex_lab.cortex cortex_lab.snowpark
snowflake snowflake.cortex snowflake.snowpark.functions

Toute autre valeur lève ValueError. Usage :

from cortex_lab.compat import complete, sentiment, classify_text
from cortex_lab.compat import snowpark_functions as F

Les paquets snowflake-ml-python et snowflake-snowpark-python ne sont pas des dépendances du labo : la valeur snowflake ne fonctionne que là où ils sont installés.

Branchement

  • Connexion DuckDB du labo : lab.duckdb_connect.connect(..., cortex=True) appelle register.
  • dbt-duckdb : plugins: [{module: cortex_lab.dbt_plugin}] dans le profil ; Plugin.configure_connection appelle register sur chaque connexion dbt. Le paquet est installé en mode éditable ([tool.hatch.build.targets.wheel]), donc uv run dbt le trouve sans PYTHONPATH.
  • Ingestion : ingest/classify.py appelle le cœur ai_classify, ingest/mapping.py appelle le cœur ai_complete.

Ajouter une fonction

Par la commande /new-cortex-function <NOM> (.claude/skills/new-cortex-function/SKILL.md), avec un PLAN.md approuvé qui couvre la fonction :

  1. Lire la page de référence https://docs.snowflake.com/en/sql-reference/functions/<nom>.
  2. Écrire le contrat dans references/contracts.md (signature, arguments optionnels, forme exacte du retour, erreurs, limites, modèle géré ou argument de modèle, existence en Snowpark), avec l'URL et la date de lecture.
  3. Tests d'abord (agent test-gate) avec le faux client tests/cortex_lab/fake_litellm.py.
  4. Implémentation (agent implementer) : cœur dans cortex_lab/core/functions/, alias dans routing.py si modèle géré, limites dans limits.py, UDF _ai_<nom> et entrée _ARITY dans cortex_lab/duckdb/__init__.py, macros dans macros.sql, façades snowpark.py et cortex.py si l'équivalent existe, alias LiteLLM créé sur le NUC.
  5. /gate.

Fonctions dont la page a été lue le 2026-10-23 mais qui ne sont pas reproduites (fonctions sur fichiers, décision A1) : AI_PARSE_DOCUMENT, TO_FILE. Vue d'ensemble : https://docs.snowflake.com/en/user-guide/snowflake-cortex/aisql (lue le 2026-10-23 ; les fenêtres de contexte par modèle n'y figurent pas).

Écarts connus

  • AI_COMPLETE : forme chaîne seulement ; retour VARCHAR dans DuckDB ; Cortex Guard non reproduit.
  • AI_COMPLETE, cache des réponses : depuis le 2026-09-29, Snowflake réutilise, au mieux et dans une même requête, les réponses d'appels texte identiques et déterministes, sans les facturer. Le labo appelle le modèle à chaque ligne : écart nouveau, à traiter par avenant (le contrat de retour est inchangé, le nombre d'appels et le coût diffèrent).
  • Modèles legacy du 2026-08-12 : un compte qui n'utilisait pas un modèle avant cette date ne peut plus l'appeler. Le labo n'embarque aucun nom de modèle Snowflake par défaut ; chez le client, le modèle de CORTEX_COMPLETE_MODEL doit être hors de la liste de https://docs.snowflake.com/en/release-notes/bcr-bundles/un-bundled/bcr-august-model-deprecations (lue le 2026-10-24).
  • snowflake.cortex.sentiment renvoie un float, pas l'objet d'AI_SENTIMENT (fidèle au paquet Snowflake, mais à ne pas confondre).
  • CREATE AI FUNCTION : écart connu, aucune instruction de ce type n'est reconnue par DuckDB au labo. Description, droits et conséquence pour le code du client dans portabilite-snowflake.md, ligne « CREATE AI FUNCTION absente du labo ».
  • AI_CLASSIFY : le labo refuse plus de 100 catégories pour toutes les formes, alors que la page SQL n'en fixe aucune pour le texte (décision S1, la page Snowpark impose 2 à 100) ; plus strict que Snowflake, donc sans risque de portabilité.
  • AI_SUMMARIZE : comportement d'erreur inféré de BCR-2184 (NULL par ligne, objet avec return_error_details), à confirmer chez le client ; la longueur et la forme du résumé dépendent du modèle local.
  • multilingual-e5-large : servi par bge-m3 comme les autres noms de 1 024 dimensions, sans la fenêtre de 512 jetons de Snowflake ni les préfixes query: et passage: du modèle d'origine.
  • Entrées texte seulement : images et fichiers (TO_FILE) non reproduits ; l'objet PROMPT n'est consommé que par AI_FILTER (pas par AI_CLASSIFY ni AI_COMPLETE).
  • Fenêtre de contexte : celle du modèle local, bien plus petite que celle des modèles Snowflake pour AI_TRANSLATE (100 000 jetons) et AI_SUMMARIZE (128K à 1M jetons selon la région) ; l'erreur est par ligne, jamais une troncature.
  • Vecteurs d'AI_EMBED : mêmes dimensions que Snowflake pour les noms de 1 024, valeurs jamais comparables à celles du client ; un vecteur DuckDB est un FLOAT[1024], pas un VECTOR.
  • AI_REDACT du labo n'est pas un contrôle de sécurité : le modèle local manque des PII.
  • Types de retour des macros : une macro DuckDB n'a qu'un type de retour par surcharge, d'où des JSON ou des VARCHAR là où Snowflake renvoie un type selon return_error_details (voir plus haut).
  • Comptage de jetons estimé (4 caractères par jeton, AI_COUNT_TOKENS comprise), pas de tokenizer du modèle géré.
  • GET renvoie VARCHAR au lieu de VARIANT.
  • La qualité des réponses diffère (modèle local qwen3:8b) : les tests vérifient le contrat, jamais le contenu.