> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cfosilvia.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Silvia orchestrator integration

# silvia-1-1 in chat-turn.ts: integration checklist

Goal: a chat turn whose `effectiveModel` is `silvia-1-1` runs on our vLLM
endpoint through the adapter in `lib/inference/silvia-chat/`, and the browser
sees the same SSE stream it sees today. Every branch below keys on
`isSilviaChatModel(model)` from `lib/inference/silvia-chat/model.ts`.

Line numbers are for `lib/claude/chat-turn.ts` on branch
`silvia-orch/plumbing` at commit `c15186711d` (6,849 lines). Re-check them
after rebasing.

## Already done outside chat-turn.ts (plumbing lane)

| What | Where | Effect in chat-turn.ts, no edit needed |
| - | - | - |
| `modelSupportsAdaptiveThinking('silvia-1-1') === false` | `lib/router/model-router.ts` | line 2819 gives `thinkingSupported = false`, so `resolveThinkingEffort` returns `off`/`default`, `thinkingConfig = null`, no `thinking`/`output_config` at 4880-4886, and `thinking_supported: false` is logged at 6427 and 6496 |
| `modelSupportsCompaction('silvia-1-1') === false` | same | 4813 skips the `compact_20260112` edit and 4858 skips the `compact-2026-01-12` beta |
| `modelSupports1MContext('silvia-1-1') === false` | same | 4855 skips `context-1m-2025-08-07`; 4432 sizes limits at 200k/160k (see item 6 to tighten) |
| `modelContextTokenLimits(model)` (new) | same | `{ maxContextTokens: 131072, safeTokenLimit: 110592 }` for silvia; Opus 1M/950k; Haiku 200k/160k |
| `silviaChatUsers` / `isSilviaChatUser` + `modelForClassAndCohort` (new) | same | silvia-1-1 only for listed Clerk ids (default Jacob); see Router |
| `normalizeModelKey('silvia-1-1') === 'quant-analyst'` | `lib/claude/model-pricing.ts` | `cost_usd` on the completion event (6476) prices at the analyst rate |
| `public.normalize_model_key` routes `silvia-%` to `quant-analyst` | `supabase/migrations/20261013_02_normalize_model_key_silvia_chat.sql` | the daily rollup and the member USD cap price silvia turns at the analyst rate. Not applied to any database yet |

## Checklist for chat-turn.ts

Each item: lines, what is there, what to do.

1. **Imports (top of file, near 71-97).** Add
   `isSilviaChatModel`, `SILVIA_CAPABILITIES` from
   `@/lib/inference/silvia-chat/model`, the stream factory from
   `@/lib/inference/silvia-chat/create-stream` (stream lane), and
   `modelContextTokenLimits` from `@/lib/router/model-router`.

2. **Stream creation, `createAndConsumeStreamWithFallback` 1791-1797.** Today:
   `usedBedrock ? createBedrockStream(...) : createStream(streamParams, betaFlags)`.
   Add a first branch: `!usedBedrock && isSilviaChatModel(currentModel)` ->
   the silvia stream factory with `streamParams` (it drops betas, cache
   markers, thinking, context\_management, speed, container itself). The
   returned `SilviaMessageStream` must be accepted by `consumeStream` (1431):
   widen its `stream` param type to
   `ReturnType<typeof anthropic.messages.stream> | SilviaMessageStream`, or
   cast at the call site. consumeStream itself needs no other change.

3. **Fallback model, `createAndConsumeStreamWithFallback` 1740-1766 and the
   call site 4964-4971.** Today `fallbackModel` is
   `DEFAULT_LLM_CONFIG.fallbackModel` (`claude-sonnet-5`). For a silvia turn
   pass `fallbackModel: DEFAULT_LLM_CONFIG.model` (Opus 4.8), so the
   `model_fallback` path lands on Opus. `streamParams` were built for silvia
   (no thinking, no betas), which is a valid Opus request, so the fallback runs
   Opus with thinking off. That is acceptable for v1. If you want thinking on
   the fallback: add `fallbackThinkingSupported` to `StreamWithFallbackConfig`
   (290-307), use it in the re-clamp at 1752-1756, and when it is true set
   `streamParams.thinking`/`output_config` from `getThinkingConfig(...)`
   (the existing code at 1759 only rewrites `output_config` if it already
   exists, so it does nothing for a silvia turn).

4. **Retry count for silvia, 1729-1737 and 4964.** Default `maxRetries: 2`
   means three silvia attempts (1s, 2s backoff) before Opus, and this repeats
   on every loop because `streamParams.model` is rebuilt from `effectiveModel`
   at 4784 each iteration. Pass `maxRetries: 0` (or 1) for silvia, and after a
   loop returns `usedFallback === true` with a silvia `effectiveModel`, set
   `effectiveModel = DEFAULT_LLM_CONFIG.model` for the rest of the turn (after
   5030\). Persist that through the checkpoint (6171) so resumed slices stay on
   Opus.

5. **Error classification, `isRetryableError` 386-443 and
   `isTransientConnectionError` 331-371.** Only 429/500/529 (or the matching
   `type`) are retryable, and a connection error must have no `status`.
   `SilviaStreamError` with `type: 'timeout_error'` and a numeric status is
   neither, so the turn fails with no fallback. Either the stream lane emits
   timeouts with `name: 'APIConnectionTimeoutError'` and no `status` (then
   1857 fails over to Bedrock Opus once), or add
   `isSilviaChatModel(currentModel) && error?.type === 'timeout_error'` as
   retryable here. Same question for `invalid_request_error` 400 (context
   overflow): today it throws immediately at 2000-2003. For a silvia turn,
   treat it as fallback-worthy, since Opus has a far bigger window.

6. **Token counting and truncation, 4432-4539.** Replace the two constants at
   4433 and 4435 with `modelContextTokenLimits(effectiveModel)`. The
   `countTokens` calls at 4440-4453 and 4492-4494 send
   `model: effectiveModel`; Anthropic rejects `silvia-1-1`, the catch at 4530
   logs and moves on, so truncation never runs for silvia. Use
   `model: isSilviaChatModel(effectiveModel) ? DEFAULT_LLM_CONFIG.model : effectiveModel`
   in `tokenCountParams` (Claude's tokenizer is a proxy; the 10% margin in
   `modelContextTokenLimits` covers the gap, and `context-budget.ts` still does
   the final fit at the wire).

7. **Static turn config, `deriveTurnStaticConfig` 2404-2850.**
   * 2810 `cacheableSystem`, 277-290 `getCacheableTools`, and the message
     markers at 4789-4791: leave as is, the adapter drops `cache_control`.
     Optional: skip `applyMessageCacheControl` when silvia to keep params clean.
   * 2813 `isFastMode`: force `false` when `isSilviaChatModel(effectiveModel)`.
     The router never picks silvia for a fast-mode request (suppressed), but a
     resumed slice or a future pin could; also stops the `fast_mode` SSE at
     4307-4318.
   * 2819-2827 thinking: nothing to do (item in the table above).
   * 2831-2834 skills: set `skillsEnabled = false` for silvia. The
     `code_execution_20250825` server tool (app/api/claude/skills.ts) and the
     `container` (4889-4891) are Anthropic-hosted; silvia cannot run them.
     Check whether the system prompt then still describes skills.

8. **Per-loop request build, 4782-4896.**
   * 4797-4846 `context_management`: wrap the whole block in
     `if (!isSilviaChatModel(effectiveModel))`. Compaction is already gated off;
     client-sent `clear_tool_uses` edits (4826-4841) are not, and 4869 still
     adds `context-management-2025-06-27`. The adapter drops them, so this is
     log hygiene, not correctness.
   * 4849-4896 betas: with items 7 and 8 done, `betaFlags` is empty for silvia.
   * 4874-4877 fast mode: covered by item 7.

9. **Fallback notice, 5040-5070.** The `model_fallback` SSE and PostHog
   `MODEL_FALLBACK` already fire when silvia falls back; no change. The client
   ignores the event today.

10. **Routing, 3846-3899.** No code change needed (see Router below).
    `effectiveModel` comes from `decideChatModel`, which now applies the
    internal-only gate.

11. **Usage and cost, 5473-5530 and 6456-6497.** The adapter's
    `finalMessage().usage` must fill `input_tokens`/`output_tokens` and zero the
    cache fields; no `iterations`. Cost is priced by `finalModelUsed`, already
    correct. `cost_usd` is nulled on any fallback turn (6476), as today.

12. **max\_tokens, 4785 and 5584.** `DEFAULT_LLM_CONFIG.maxTokens` is 32,768;
    silvia reserves 8,192 (`SILVIA_MAX_OUTPUT_TOKENS`). The adapter clamps. A
    clamped reply that ends with `stop_reason: 'max_tokens'` and no tool call
    throws `max_tokens_cutoff` at 5584, as for Claude.

13. **Bedrock, 547-615 and 1768-1783, 1857-1876, 1883-1912.** No change.
    Bedrock is only reached after the Anthropic fallback, or on a connection
    error, and `createBedrockStream` overwrites `model` with the Bedrock Opus id.

14. **Images, 497-515 and 3809.** No change. Downscaling to 2,000 px is fine
    for silvia (`SILVIA_CAPABILITIES.images: true`).

## Router: is the env var enough?

Yes, with conditions. `CHAT_ROUTER_MODEL_SHORT=silvia-1-1` routes the `short`
class with no code change, but only when all of these hold
(`lib/router/model-router.ts::decideChatModel`):

* `CHAT_ROUTER_MODE=live`. In `shadow` (the code default) the model never
  changes. Confirm the prod value in Vercel before relying on it.
* Confidence >= `CHAT_ROUTER_MIN_CONFIDENCE` (default 0.65).
* Not suppressed: research mode, fast mode (`body.speed === 'fast'`),
  attachments or inlined files, video intent, or an explicitly requested model.
  Each keeps the baseline (Opus 4.8).
* Demo mode never calls the router (chat-turn 3846).

**Superseded (2026-10-14): gated by user, not by internal cohort.** silvia-1-1
serves only the Clerk ids in `silviaChatUsers()` (lib/router/model-router.ts):
Jacob's account by default, replaced by `SILVIA_CHAT_USERS` (comma list; empty
string turns silvia off). Those users run every turn on silvia-1-1 with no router:
no classifier and no suppressions (research, fast mode, attachments, video and
explicit models included). A silvia failure still falls back to Opus. A
`CHAT_ROUTER_MODEL_<CLASS>=silvia-1-1` override never reaches anyone outside
the list, internal or not. No env change is needed for Jacob to use it.

## Other readers of the model string (checked, no change needed)

* `lib/token-limit.ts` / `lib/token-limit-client.ts`: the USD cap calls
  `get_user_cost_in_window`, which prices chat rows by
  `COALESCE(router_effective_model, model)` through `normalize_model_key`. The
  new migration covers it. Note: a silvia turn that fell back to Opus is still
  priced as silvia (router\_effective\_model wins). Same rate today, so no
  billing difference.
* `ai_token_usage_daily` rollup (`compute_ai_token_usage_daily`): same pricing
  path, covered.
* PostHog `RESPONSE_COMPLETED` (6453-6512): `model_used`, `cost_usd`,
  `thinking_*`, `genui_*` all correct with no change.
  `normalize-genui-fences` does not branch on the model; it will report
  silvia's GenUI slips, which is the signal we want.
* `ai_chat_logs.thinking_supported` / `thinking` (6420-6428): `false`/`off`.
* `lib/router/log-decision.ts`: logs `router_effective_model = silvia-1-1`.
* `lib/fine-tuning-collector.ts`: records `model` verbatim, no branching.
* `lib/judge`: judges with its own model (quant-analyst 1.0); does not read the
  chat model.
* `lib/suggested-prompts.ts`: no model branching.
* `app/api/claude/internal/route.ts` 383/392: only checks Opus and Sonnet.

## Risks

1. **Silent no-fallback on timeouts** (item 5). Highest risk: a Modal cold
   start or a stalled vLLM request kills the turn instead of falling back.
2. **Retry cost per loop** (item 4). With an unhealthy endpoint every loop pays
   three failed attempts plus backoff before Opus.
3. **Opus fallback runs without thinking** (item 3) and without the 1M beta.
   Quality on fallback turns is below a normal Opus turn.
4. **Training on our own outputs.** Fine-tuning traces from silvia turns land
   in `fine_tuning_traces` with `model = 'silvia-1-1'`. Exclude them (or tag
   them) in any training cut, or the next model trains on the current one.
5. **Judge self-preference.** The chat judge is Silvia Analyst 1.0 (a Qwen
   fine-tune). Scores comparing silvia-1-1 against Opus may favor silvia.
   Compare within judge\_model and spot-check with a Claude judge.
6. **Mixed history in one turn.** If loop 1 falls back to Sonnet/Opus and
   loop 2 returns to silvia, the history contains thinking blocks and possibly
   `server_tool_use`. The request converter must drop those block types.
7. **Pricing row only in prod.** `model_pricing.quant-analyst` was added by
   hand; the new migration inserts it only when missing. Without it,
   `model_cost_usd` returns NULL for analyst and silvia rows.
8. **Migration not applied.** Until `20261013_02` runs, SQL meters silvia-1-1
   as `opus`. Same rate, so the cap is unaffected, but the model key in any
   per-model report is wrong.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.