Appendix C — Generating Features with Large Language Models
Chapter 13, Large Language Models and Recommendation, explained the enhancer role: keep a fast collaborative model on the serving path, and use a large language model (LLM) offline to manufacture the semantic signal that model lacks. This appendix is the hands-on counterpart — the actual prompt that writes a profile, the embed step that turns it into a vector, the way that vector is fused into the backbone (the collaborative model that serves; LightGCN here), and the engineering harness that makes it a pipeline rather than a demo.
What this is and isn’t. It is the RecSys-applied slice of LLM orchestration: using LLM calls to produce recommender features. It is not a general LLM-agent or prompt-engineering tutorial — for the broader roles (LLM-as-recommender, generative, agentic) see Chapter 13.
Recency note (read this). This is the fastest-moving material in the handbook — model names, context windows, prices, and the features of application programming interfaces (APIs), such as JavaScript Object Notation (JSON) mode and batch endpoints, change every few months. So the appendix teaches the shape that stays stable — prompt → profile → embed → fuse → cache — and points to Implementation Choices for the dated specifics. Treat every concrete model name or price as illustrative and re-verify before you build. The API examples reflect provider documentation dated June 2026.
The appendix assumes:
- Representation Learning & the Transformer — what an LLM and a text embedding are; the frozen text encoder (one whose weights are not trained further here) is the thing we call.
- Large Language Models and Recommendation §2.3 / §3 — the enhancer role and RLMRec (Representation Learning with LLMs for Recommendation; Ren et al., 2024: LLM writes a profile → embed → align); this appendix is a simplified version of that recipe, not a reproduction of the paper’s prompts (§2).
- Self-Supervised & Contrastive Learning §7 — the alignment mechanics, if you fuse by contrast.
- Implementation Choices — which embedding model / LLM / vector store to actually pick.
1. The recipe in one place
The whole enhancer pipeline is three offline steps and one serving fact, set out in the block below.
OFFLINE (run once, refresh on change):
for each user and item:
1. PROMPT an LLM with its raw text -> a short natural-language PROFILE
2. EMBED the profile string -> a semantic VECTOR
3. FUSE the vector into the backbone (concat as a feature, or align it)
SERVING (per request):
4. the trained backbone scores + ranks as usual; the LLM is NEVER called
The single most important property: the LLM runs offline, not at query time. You pay for it once per user/item (and again only when their text or the prompt changes), and no LLM is called at query time — the enhancer bargain of Large Language Models and Recommendation §2.3. What the serving model itself must carry depends on how you fuse the vector (§4). Everything below is how to do each step concretely and reliably. The model that ranks at serving time is the collaborative filtering (CF) backbone of the figure below.
2. Step 1 — the profile prompt
The job: turn an item’s (or user’s) raw text into a short, structured profile the rest of the pipeline can embed. Two rules make this reliable.
Ask for a short profile, not a long essay. A 1–2 sentence profile + a few tags embeds into a focused vector; a long generation tends to dilute the signal (common practice, not tested here) and costs more tokens (a token is a word piece, roughly three-quarters of an English word, and the unit an LLM is billed in). Item prompt:
System: You write concise taste profiles for a movie recommender.
User: Given the item below, write a two-sentence profile capturing its
themes, mood, and the kind of viewer who would love it. Then list 4 tags.
Item: "Titanic" (1997) — genres: Romance, Drama;
a period romance set on the doomed liner.
Demand structured output. Free text is a parsing hazard; ask for JSON. Most APIs have a “JSON mode” / schema-constrained decoding that makes parseable output very likely (a reply cut off at the token limit can still fail, so §5 validates and retries):
{"profile": "A sweeping period romance set against disaster, emotionally
driven and character-first, with broad mainstream appeal. Best for viewers who
love grand, tear-jerking love stories more than plot mechanics.",
"tags": ["romance", "period-drama", "tragedy", "mainstream"]}The user profile is the same move over the user’s history:
User: Here is a user's rated history. Write a two-sentence taste profile
and 5 tags, as JSON.
History: Titanic (5), The Notebook (5), La La Land (4), Die Hard (2).
The reply:
{"profile": "Strongly prefers emotional, romance-driven films and musicals.
Lukewarm on action; values character and mood over spectacle.",
"tags": ["romance", "musical", "drama", "character-driven", "low-action"]}These prompts simplify RLMRec’s: its item prompt returns a summary plus a short reasoning as JSON, and its user prompt reads the profiles and reviews of the user’s items, not raw ratings. A user with no history has no rated items to prompt on: a variant then runs over whatever text exists (a sign-up form, stated interests) as an ingestion job at sign-up, not a request-time call; with no text there is nothing to write a profile from.
That is the entire creative step. The profile is deterministic-enough (a low sampling temperature, the setting that makes the model’s word choice less random, gives nearly the same words for the same input) and cheap (a few hundred tokens), and it exists even for an item with zero interactions, the cold-start payoff, the case of a user or item with no interactions yet (From Graphs to LightGCN §14: a pure-CF model has no vector for an unseen node; its text gives it one, and whether a model can use that vector is the fusion question of §4).
3. Step 2 — profile → embedding
A profile is still text, and a ranking model can only do arithmetic on numbers. So encode each profile string into a fixed-length semantic vector with a frozen sentence-embedding model (frozen: used as released, not trained further; which one is an Implementation Choices §2 question — a small, permissively licensed embedder such as a model from the e5, BGE or Qwen-Embedding families of released text encoders; \(\ell_2\)-normalize the output so dot product = cosine, as in Training and Serving a Recommender §7).
The vectors now place similar tastes near each other, which is the whole point. Illustration (2-D, \(\ell_2\)-normalized, romance-vs-action axes): Ann’s romance-leaning profile embeds near \([0.994,0.110]\), a second romance lover Dave near \([0.970,0.243]\), and the action lover Cara near \([0.217,0.976]\). Because the vectors are unit length, the dot product is the cosine: \[ \begin{gathered} \cos(\text{Ann},\text{Dave})=(0.994)(0.970)+(0.110)(0.243)=0.99,\\[2pt] \cos(\text{Ann},\text{Cara})=(0.994)(0.217)+(0.110)(0.976)=0.32 . \end{gathered} \] Ann and Dave land almost on top of each other from their text alone — and they share no interactions at all (Dave appears in no rating in this book) — while the action lover is far. Dave’s profile came from the interests he typed at sign-up, since the §2 recipe over rated history has nothing to read for him. That is similarity a pure collaborative model could not see for a cold user, and it exists only because Dave left some text: a cold user with no text stays cold.
These are semantic-space vectors, not the matrix factorization (MF) factors of Traditional Recommender Systems §5. The embedding lives in the LLM encoder’s space, which is not the same as the backbone’s collaborative latent space. They have to be combined before a model can use both, which is what §4’s fusion step does — either by widening the CF model’s input (concat: stack the two vectors into one wider input, optionally after a learned adaptor as in KAR, the open-world knowledge-augmented recommendation framework of Xi et al., 2024) or by aligning the two spaces.
4. Step 3 — fuse the vector into the backbone
Each user and item now has two lists of numbers, one learned from clicks (Ann’s, Cara’s) and one read from its text, and the ranking model must use both. There are two standard ways to put the semantic vector to work; pick by how much you want to change the backbone.
- Concatenate as a feature (simplest; worked score below). Treat the semantic vector \(\mathbf s_i\) (length \(d'\)) as extra input features alongside the identifier (ID): the model’s input grows from the collaborative vector \(\mathbf e_i\) (length \(d\)) to the stacked \([\mathbf e_i;\mathbf s_i]\) (length \(d+d'\)). This is the feature-augmentation route of KAR (Xi et al., 2024), where a learned adaptor first maps the LLM’s vector into the recommendation space and shrinks it. No change to the training objective; you just widen the input. In a factorization machine (FM) or a multi-layer perceptron (MLP) scorer (Traditional Recommender Systems §8) the new features then interact with the others; in a plain dot-product model such as LightGCN they do not (below). It is the smaller change, so the usual first try (the table below compares the two).
- Align by contrast (RLMRec). Keep the collaborative embedding \(\mathbf e_i\) and the semantic embedding \(\mathbf s_i\) as two views of the same node (a node is one user or one item) and train \(\mathbf e_i\) so the two agree. RLMRec (Ren et al., 2024; its contrastive variant, RLMRec-Con; the generative RLMRec-Gen is not used) maps \(\mathbf s_i\) into the collaborative space with a small neural network \(g\) (the projection, written \(\sigma_\downarrow\) in the Self-Supervised & Contrastive Learning chapter: an MLP, needed because \(d'\) and \(d\) differ), measures agreement by cosine similarity, and adds one term \(\mathcal L_{\text{info}}\) (an information noise-contrastive estimation (InfoNCE) term; Self-Supervised & Contrastive Learning §7) to the recommender’s own loss \(\mathcal L_R\) (Bayesian Personalized Ranking, BPR, for LightGCN): \[ \mathcal L=\mathcal L_R+\mathcal L_{\text{info}},\qquad \mathcal L_{\text{info}}=-\log\frac{\exp\!\big(\cos(g(\mathbf s_i),\mathbf e_i)\big)} {\sum_j\exp\!\big(\cos(g(\mathbf s_j),\mathbf e_i)\big)} \] with \(\mathbf e_i\) the collaborative vector, \(\mathbf s_i\) the semantic vector, \(g\) the learned map, \(\cos\) the cosine, and \(j\) running over the nodes of the batch (the group processed together in one training step).
The numerator of \(\mathcal L_{\text{info}}\) is the pull: node \(i\)’s own two views. The other nodes’ views \(g(\mathbf s_j)\) in the denominator are the push (numbers in the worked term below). The paper writes no temperature, so the loss above has none (it is the case \(\tau=1\)). A temperature \(\tau\) is a number that divides every cosine inside the exponentials: a small \(\tau\) sharpens the contrast between views and a large one flattens it; the Self-Supervised & Contrastive Learning chapter, pointed to above, adds one. It is unrelated to the low sampling temperature of §2.
A worked concat score. For a dot-product scorer the stacked score splits into two separate sums, \([\mathbf e_u;\mathbf s_u]\cdot[\mathbf e_i;\mathbf s_i]=\mathbf e_u\!\cdot\!\mathbf e_i+\mathbf s_u\!\cdot\!\mathbf s_i\), with \(u\) a user and \(i\) an item. Take Ann (\(u\)) with a trained collaborative vector \(\mathbf e_{\text{Ann}}=(0.9,0.2)\) (the numbers the align example below reaches after training), against a romance item \(i\) with \(\mathbf e_i=(0.5,0.1)\) whose text embeds at the same point as Dave’s profile in §3, \(\mathbf s_i=(0.970,0.243)\). The collaborative part is \(0.45+0.02=0.47\), the semantic part is Ann’s \(\mathbf s_{\text{Ann}}=(0.994,0.110)\) against the item’s \(\mathbf s_i\), \(0.99\), and the score is \(0.47+0.99=1.46\). No term multiplies a collaborative number by a semantic one: the two spaces add up and never meet.
With a frozen \(\mathbf s\) the \(0.99\) is a fixed content score, which training cannot change; only a learned adaptor (KAR) or an FM or MLP scorer lets the model re-weigh it. One case is enough to show the split; the identity holds for any vectors under a dot-product scorer.
A worked align term, two nodes. Take the §3 vectors for Ann and Cara, with \(g\) already trained so that \(g(\mathbf s_{\text{Ann}})=(0.994,0.110)\) and \(g(\mathbf s_{\text{Cara}})=(0.217,0.976)\). Early in training Ann’s collaborative vector is \(\mathbf e_{\text{Ann}}=(0.3,0.9)\), which points toward Cara’s profile: its cosine is \(0.42\) with her own and \(0.99\) with Cara’s, so the term is \[ \mathcal L_{\text{info}}=-\log\frac{e^{0.42}}{e^{0.42}+e^{0.99}}=1.02 . \]
After training moves it to \((0.9,0.2)\) the two cosines swap (\(0.99\) and \(0.42\)) and the term falls to \(0.45\). Two points suffice here because only the cosines with her own and Cara’s profile change, and they swap; the term cannot reach \(0\) while Cara’s view stays in the denominator. The loss pays for pulling \(\mathbf e_{\text{Ann}}\) toward her own profile and for pushing it away from Cara’s; pulling alone is satisfied by mapping every node to the same vector, which the push rules out (if every node sat at one point, the term would be the log of the batch size, here \(\log 2=0.69\), worse than \(0.45\)). This shapes the collaborative space with semantics rather than appending to it, and it is a training loss on \(\mathbf e_i\) and \(g\), with no serving-time term.
The decisive practical difference is what the serving path must carry, and what a new item gets — not whether the LLM runs (it never does at query time):
| Concat (early fusion) | Align (RLMRec) | |
|---|---|---|
| Training objective | unchanged — just a wider input | recommender loss + the contrastive term above (Self-Supervised & Contrastive Learning §7) |
| Embedding dimension | grows to \(d + d'\) (the stacked \([\mathbf e_i;\mathbf s_i]\)) | unchanged (\(d\)) |
| Semantic vector \(\mathbf s_i\) at serving | needed — stored, and used in every score | not needed to score — \(\mathbf e_i\) was trained to agree with it |
| A brand-new item | has its \(\mathbf s_i\) as soon as its text has been through steps 1–2 (a default until then), but its \(\mathbf e_i\) half is still untrained (say zeros), so the score rests on \(\mathbf s_u\!\cdot\!\mathbf s_i\) alone, a case training never saw by itself (not tested here) | has no trained \(\mathbf e_i\) (no interactions to train it): score it with a fallback such as \(g(\mathbf s_i)\) in place of \(\mathbf e_i\) (a design choice the RLMRec paper does not describe), or retrain |
| Reach for it when | the smaller change, or a feature-rich FM or click-through rate (CTR) backbone | you want a lean serving model and the items already have interactions to train \(\mathbf e_i\) |
Concat keeps the semantic vector in the loop forever (a wider input, an extra table to store and join at scoring), whereas align lets the contrastive term teach the collaborative space the semantics during training, so at query time you ship the same \(d\)-dim LightGCN (From Graphs to LightGCN; \(d\) and \(d'\) as defined in the two routes above) with nothing extra on the serving path — at the price that an item with no interactions yet has no trained vector.
RLMRec compares itself with KAR, which feeds the semantic vector in as input features: the concat route in this appendix’s terms, though KAR itself uses a jointly trained adaptor, and the paper does not say whether its KAR baseline kept it. It reports align ahead, with relative gains in Recall@20 (the share of a user’s held-out liked items that appear in the top 20 of the list; Evaluation Metrics §4) over the base model on Amazon-book (Datasets & Benchmarks §2): with LightGCN, \(+5.1\%\) for RLMRec-Con, the align route, against \(+0.3\%\) for KAR; with the self-supervised graph learning (SGL) backbone, \(+3.7\%\) against \(-2.5\%\) (Recall@20 \(0.1473\) for the base model, \(0.1436\) for KAR; its Table 2, as printed). That is one paper’s result on its datasets, so this appendix does not rank the routes in general.
The shared benefit is a semantic prior (what the text says about a node before any clicks) that is expected to matter most where the collaborative signal is weakest, such as the long tail (the many items that each have few interactions). Take the romance item of the worked concat score above: its text alone, with no clicks, sits at cosine \(0.99\) from Ann’s taste. That is an expectation, not a result: RLMRec’s stated motivation is sparse, noisy and biased implicit feedback, its paper reports no popularity breakdown (Large Language Models and Recommendation §3 and §5), and this appendix does not test it. For a brand-new item the two differ exactly as the table says.
5. The harness — turning prompts into a pipeline
A single prompt in a notebook is a demo. Generating profiles for millions of users and items, reliably and affordably, is an orchestration problem — the “harness” around the LLM call. The pieces that matter:
- Versioned prompt templates. Keep the prompt in code as a parameterized, versioned template (not pasted strings). When you change the prompt, the profiles change — so the version is part of the cache key.
- Structured output + validation. Request JSON, then validate against a schema and retry on a parse/validation failure (with a stricter reminder). Never let an unparseable generation enter the feature store (the table described in the last bullet below).
- Batching & concurrency. Profiles are independent, so process them in batches (many items per request where the API allows) and with bounded concurrency (a worker pool), not one blocking call at a time. This is where the wall-clock lives.
- Caching & idempotency. Hash each input (the item text + the prompt version) and record the hash in a cache that indexes the stored profile; regenerate only when the input or prompt changes. A catalog is mostly static, so item profiles are generated once; a user’s history keeps growing, so user profiles need a refresh policy (say, regenerate after \(N\) new interactions or weekly, whichever comes first — a design choice).
- Cost & rate limits. Budget up front: (number of users + items) × (tokens per profile, prompt plus reply) × (price per token). At an illustrative blended $0.50 per million tokens for prompt and reply together, 2 million users and items at an assumed 330 tokens each is \(660\) million tokens, about $330 once; refreshing \(1\) million active users weekly is \(330\) million tokens, about $165 a week. The same 330 tokens spent at query time, for \(10\) million requests a day, would be \(3.3\) billion tokens, about $1,650 a day. So the 330 is an assumption and the budget a model, not a measurement.
- What the budget leaves out. Three things would raise a real bill: a user’s history prompt is longer than \(330\) tokens, replies are billed above prompts (a blended price hides the split), and the embedding step is billed on top. Because generation is offline and scheduled, it is a batch cost you can run on cheap/off-peak capacity — not a per-request tax (the enhancer advantage, Large Language Models and Recommendation §2.3).
- An offline feature store. The output is a table:
id → (profile, vector, prompt_version, input_hash, generated_at). The serving path reads vectors from this store and never calls the LLM; a scheduled job refreshes profiles for new or changed items and users.
The loop below caches by hash, validates every reply, and stores only valid profiles, writing the store before the cache so that a crash leaves nothing cached that was not stored.
generate_profiles(items, prompt_v): # B = items per LLM request
key(x) = hash(x.text, prompt_v) # cache key: text + prompt version
for batch in chunks(items, size=B): # batching
todo = [x for x in batch
if cache.miss(key(x))] # idempotency
if not todo: continue # all cached: no LLM call
outs = llm.batch(render(prompt_v, todo),
json_schema=PROFILE) # JSON shape of section 2
for x, out in zip(todo, outs):
if not valid(out):
out = retry(x, prompt_v, max=2) # validate + bounded retry
if not valid(out):
log_and_skip(x); continue # never store a failed profile
store.put(x.id, out.profile, embed(out.profile),
prompt_v, key(x), now) # upsert: safe to repeat
cache.put(key(x)) # key index over the store
# skipped items stay uncached: the next run retries them
That loop — batch, skip-cached, structured-call, validate, embed, store — is the difference between “I tried a prompt” and “I have a feature pipeline.”
6. Does the feature actually help?
Generating a profile is easy; proving it helps is the discipline. Two checks, both from Evaluation Metrics:
- Ablate. Train the backbone with and without the LLM feature on the same protocol (full-ranking NDCG@K (normalized discounted cumulative gain) / Recall@K, mean ± std over seeds, a significance test — Evaluation Metrics §11). The question is the delta, not the headline.
- Stratify by popularity. Report the gain on head vs. tail items (the few most-interacted items against the many rarely-interacted ones) separately (Evaluation Metrics §12.6). The semantic feature should help most on the cold tail (expected, not tested here); if the only lift is on the head, the profiles are adding little the collaborative signal didn’t already have.
A worked ablation. Train two identical backbones — same LightGCN, same dimension, same five seeds — differing only in whether the semantic feature is fused. Say NDCG@10 comes out:
- no feature: \(0.28,\,0.30,\,0.29,\,0.31,\,0.32\) \(\to\) mean \(\mathbf{0.300}\)
- + LLM feature: \(0.30,\,0.31,\,0.32,\,0.33,\,0.34\) \(\to\) mean \(\mathbf{0.320}\)
The headline gap is \(+0.020\), but the right test is paired, because the two runs share seeds: the per-seed deltas are \([+0.02,+0.01,+0.03,+0.02,+0.02]\), mean \(\bar d=0.020\), sample std \(0.00707\), so the standard error (SE) is \(0.00707/\sqrt5=0.00316\) and the paired-\(t\) (Evaluation Metrics §11d) is \[t=\frac{0.020}{0.00316}=6.3\] on \(4\) degrees of freedom (d.f.), giving a two-sided \(p=0.003\) (the five seeds treated as independent runs) — comfortably under \(0.05\). The gain is larger than the seed-to-seed noise on this protocol (an unpaired, pooled-variance two-sided \(t\)-test on the same ten numbers gives \(t=2.0\) on \(8\) d.f., \(p=0.08\), because it ignores that each seed moved both runs together). Had the per-seed deltas been \([+0.04,+0.03,+0.02,+0.02,-0.01]\) — the same mean, one seed worse — the same test gives \(t=2.4\), \(p=0.075\), and the correct report would be no significant gain: on these five seeds the feature cannot be told apart from noise.
And the caveat from Large Language Models and Recommendation §2.3: a misaligned profile can hurt. RLMRec’s authors report that shuffling the profile vectors between nodes lowers performance (Self-Supervised & Contrastive Learning §7), and KAR’s \(-2.5\%\) (\(0.1473\to0.1436\)) on the SGL backbone (§4) shows a semantic input ending below the base model; the paper’s only comment on KAR’s weaker results is a hedged guess that input features “may not effectively align” the text with behaviour and are “more susceptible to irrelevant noise”, which it does not test. That a merely poor profile hurts is plausible but not tested here. The pipeline is only as good as the profiles and the alignment — measure, don’t assume.
7. Where this leads: profile density
This appendix used one short profile per node. Here density means how many profiles each user or item gets, counted here simply as profiles per node: this appendix has density 1, and a variant with 3 profiles per user triples the user-side generation, from \(330\) million to \(990\) million tokens per weekly refresh of §5’s 1 million users.
The open research question, as this appendix reads it, is how rich and how faithful that generated signal should be: longer multi-aspect profiles, several profiles per node, profiles distilled from reviews or knowledge, how densely to generate them against cost, and, just as much, how to align that signal so it helps rather than drowns the collaborative structure. Large Language Models and Recommendation §5 asks two nearby questions for the enhancer line, whether the semantic view aligns with real collaborative structure and whether thin-data items gain most, and lists both as open. Profile coverage and profile accuracy are this appendix’s own reading, not questions §5 names. The pipeline here is the on-ramp; how far to push density and quality is live research.
8. Glossary
| Term | Plain meaning |
|---|---|
| Backbone | The collaborative model that scores at serving time (LightGCN here; From Graphs to LightGCN), the one the LLM feature is fused into. |
| Adaptor | A small trained network that maps the LLM’s vector into the recommender’s space and shrinks it, as in KAR (§4). |
| Profile | A short LLM-written description of a user’s taste or an item’s character, used as input to an embedder. |
| Semantic embedding | A fixed-length vector encoding the meaning of the profile text (from a frozen sentence encoder), distinct from the CF latent space. |
| Enhancer pipeline | Using an LLM offline to manufacture features for a fast collaborative backbone that serves. |
| Structured output / JSON mode | Forcing the LLM to emit schema-valid JSON, so the result is very likely parseable (§5 still validates). |
| Harness / orchestration | The engineering around the LLM call — templates, validation, batching, retries, caching, cost control. |
| Idempotency | Re-running the pipeline on unchanged input does no new work (hash → cache hit), so a profile is regenerated only when its input or the prompt version changes. |
| Feature store | The table of generated id → (profile, vector) the serving path reads; the LLM is never on the serving path. |
| Alignment | Pulling a node’s semantic vector and collaborative vector together (contrastive, Self-Supervised & Contrastive Learning §7) so the two spaces agree. |
| Contrastive (InfoNCE) term | Added to the recommender’s loss: it pulls a node’s collaborative and projected semantic vectors together (the numerator) and pushes its collaborative vector away from other nodes’ semantic vectors (the denominator). |
| Projection \(g\) | The small network that maps the semantic vector into the collaborative space so the two can be compared (§4). |
| Concat (early fusion) | Append the LLM feature vector to the ID embedding — the quick first try, at the cost of a fatter serving vector. A frozen vector adds a fixed content score; only an adaptor or an FM or MLP scorer lets the model re-weigh it (§4). |
9. References & resources
Papers (the enhancer line — full citations in Large Language Models and Recommendation).
- Ren, X., Wei, W., Xia, L., Su, L., Cheng, S., Wang, J., Yin, D., & Huang, C. (2024). Representation learning with large language models for recommendation (RLMRec). WWW ’24. arXiv:2310.15950 — the LLM-writes-a-profile → embed → align recipe this appendix operationalizes. https://doi.org/10.1145/3589334.3645458
- Xi, Y., Liu, W., Lin, J., Cai, X., Zhu, H., Zhu, J., Chen, B., Tang, R., Zhang, W., & Yu, Y. (2024). Towards open-world recommendation with knowledge augmentation from large language models (KAR). RecSys ’24, 12–22. arXiv:2306.10933 — LLM-generated reasoning/knowledge condensed by an adaptor into vectors that join the other input features (the concat-style fusion). https://doi.org/10.1145/3640457.3688104
Tools (dated — see Implementation Choices for the current picks). Embedding models (Implementation Choices §2: e5 / BGE / Qwen-Embedding families), vector stores and the normalize→inner-product trick (Implementation Choices §3), and small permissive LLMs for offline feature generation (Implementation Choices §4) all live in that appendix, with its dated benchmarks and the license gate.
Provider documentation (JSON mode, batch endpoints, prices) dates from June 2026; treat all model names, prices, and API features as illustrative and re-verify before building.