Skip to content

U5: Semantic cache + pgvector + indexes#10

Open
jaturapornchai wants to merge 1 commit into
mainfrom
worktree-agent-ac18d4cf
Open

U5: Semantic cache + pgvector + indexes#10
jaturapornchai wants to merge 1 commit into
mainfrom
worktree-agent-ac18d4cf

Conversation

@jaturapornchai

Copy link
Copy Markdown
Owner

Summary

  • Add pgvector extension + semantic_cache table (ivfflat cosine index) with graceful fallback when the extension is unavailable.
  • Add hot-path performance indexes: idx_health_cooldown_active (partial), idx_gateway_recent, idx_exam_passed_recent (partial).
  • New src/lib/semantic-cache.ts library: embed via local Ollama nomic-embed-text, cosine-similarity lookup, idempotent store, stats helper. All operations no-op silently on failure.

Library is not yet wired into v1/chat/completions — follow-up unit.

Test plan

  • rtk npx next build — 0 errors, 0 warnings
  • Deploy and verify [migrate] pgvector + semantic_cache ready log line (or graceful warning if ext missing)
  • Confirm new indexes exist via \d health_logs / \d gateway_logs / \d exam_attempts

Adds pgvector extension, semantic_cache table (ivfflat cosine index),
and three WHERE-predicate/covering indexes for hot-path queries
(health cooldowns, recent gateway logs, passed exams). New
semantic-cache.ts library provides graceful lookup/store via local
Ollama nomic-embed-text; all operations no-op on failure.

Library is not yet wired into the chat completions route — follow-up.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 9, 2026 21:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an optional Postgres-backed semantic cache using pgvector (Ollama embeddings + cosine similarity) and introduces several new “hot path” DB indexes to improve query performance, while aiming to fail gracefully when pgvector/Ollama are unavailable.

Changes:

  • Add src/lib/semantic-cache.ts with embedding, similarity lookup, idempotent storage, and cache stats helpers.
  • Extend DB migrations to provision pgvector + semantic_cache (ivfflat cosine index) with fallback logging.
  • Add new performance indexes on health_logs, gateway_logs, and exam_attempts.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/lib/semantic-cache.ts New semantic cache library: embeds via Ollama and queries/stores in semantic_cache.
src/lib/db/migrate.ts Migration adds pgvector + semantic_cache table/indexes and additional performance indexes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/lib/semantic-cache.ts
Comment on lines +40 to +43
const json = await res.json();
const embedding = json.embedding as number[] | undefined;
return Array.isArray(embedding) && embedding.length > 0 ? embedding : null;
} catch {

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

embedQuery accepts any embedding length, but the DB column is vector(768). If Ollama returns a different dimension (model change, config, API change), every store/lookup will fail and silently no-op. Add an explicit embedding.length === 768 (and optionally finite-number) validation before returning/using the embedding so failures are fast and diagnosable.

Copilot uses AI. Check for mistakes.
Comment thread src/lib/semantic-cache.ts
Comment on lines +82 to +87
// Update hit count + last_used_at (fire and forget)
sql`
UPDATE semantic_cache
SET hit_count = hit_count + 1, last_used_at = now()
WHERE query = ${row.query}
`.catch(() => {});

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hit counter update keys on query, but query is not guaranteed unique (you store query.slice(0, 2000) while query_hash is computed from the full query). This UPDATE can increment multiple rows if different long queries share the same first 2000 chars. Update by a unique identifier instead (e.g., select id or query_hash and use that in the WHERE clause).

Copilot uses AI. Check for mistakes.
Comment thread src/lib/db/migrate.ts
await sql`
CREATE INDEX IF NOT EXISTS idx_health_cooldown_active
ON health_logs(model_id, cooldown_until)
WHERE cooldown_until > now()

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This partial index predicate uses now(), which is STABLE (not IMMUTABLE). Postgres will reject it with "functions in index predicate must be marked IMMUTABLE", so the index will never be created (and you'll log a warning every startup). Use an immutable predicate (e.g., cooldown_until IS NOT NULL) or replace with a normal index and keep the time comparison in the query.

Suggested change
WHERE cooldown_until > now()
WHERE cooldown_until IS NOT NULL

Copilot uses AI. Check for mistakes.
Comment thread src/lib/db/migrate.ts
Comment on lines +354 to +379
CREATE INDEX IF NOT EXISTS idx_semantic_cache_embedding
ON semantic_cache USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100)
`;
await sql`CREATE INDEX IF NOT EXISTS idx_semantic_cache_used ON semantic_cache(last_used_at DESC)`;
console.log("[migrate] pgvector + semantic_cache ready");
} catch (err) {
console.warn("[migrate] pgvector not available — semantic cache disabled:", String(err).slice(0, 150));
}

// ─── Performance indexes ─────────────────────────────────────────────────────
// Note: CREATE INDEX CONCURRENTLY cannot run inside transaction,
// so we use normal CREATE INDEX IF NOT EXISTS here
await sql`
CREATE INDEX IF NOT EXISTS idx_health_cooldown_active
ON health_logs(model_id, cooldown_until)
WHERE cooldown_until > now()
`.catch((err) => console.warn("[migrate] idx_health_cooldown_active:", String(err).slice(0, 100)));

await sql`
CREATE INDEX IF NOT EXISTS idx_gateway_recent
ON gateway_logs(created_at DESC, status)
`.catch((err) => console.warn("[migrate] idx_gateway_recent:", String(err).slice(0, 100)));

await sql`
CREATE INDEX IF NOT EXISTS idx_exam_passed_recent

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These indexes are created with plain CREATE INDEX, which can take an ACCESS EXCLUSIVE lock and block writes on large tables during startup migrations. Since runMigrations() isn't running inside an explicit transaction block, consider CREATE INDEX CONCURRENTLY (or moving index creation to an offline/one-time migration step) to reduce production impact.

Suggested change
CREATE INDEX IF NOT EXISTS idx_semantic_cache_embedding
ON semantic_cache USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100)
`;
await sql`CREATE INDEX IF NOT EXISTS idx_semantic_cache_used ON semantic_cache(last_used_at DESC)`;
console.log("[migrate] pgvector + semantic_cache ready");
} catch (err) {
console.warn("[migrate] pgvector not available — semantic cache disabled:", String(err).slice(0, 150));
}
// ─── Performance indexes ─────────────────────────────────────────────────────
// Note: CREATE INDEX CONCURRENTLY cannot run inside transaction,
// so we use normal CREATE INDEX IF NOT EXISTS here
await sql`
CREATE INDEX IF NOT EXISTS idx_health_cooldown_active
ON health_logs(model_id, cooldown_until)
WHERE cooldown_until > now()
`.catch((err) => console.warn("[migrate] idx_health_cooldown_active:", String(err).slice(0, 100)));
await sql`
CREATE INDEX IF NOT EXISTS idx_gateway_recent
ON gateway_logs(created_at DESC, status)
`.catch((err) => console.warn("[migrate] idx_gateway_recent:", String(err).slice(0, 100)));
await sql`
CREATE INDEX IF NOT EXISTS idx_exam_passed_recent
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_semantic_cache_embedding
ON semantic_cache USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100)
`;
await sql`CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_semantic_cache_used ON semantic_cache(last_used_at DESC)`;
console.log("[migrate] pgvector + semantic_cache ready");
} catch (err) {
console.warn("[migrate] pgvector not available — semantic cache disabled:", String(err).slice(0, 150));
}
// ─── Performance indexes ─────────────────────────────────────────────────────
// runMigrations() is not inside an explicit transaction, so prefer
// CREATE INDEX CONCURRENTLY IF NOT EXISTS to reduce write blocking during startup
await sql`
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_health_cooldown_active
ON health_logs(model_id, cooldown_until)
WHERE cooldown_until > now()
`.catch((err) => console.warn("[migrate] idx_health_cooldown_active:", String(err).slice(0, 100)));
await sql`
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_gateway_recent
ON gateway_logs(created_at DESC, status)
`.catch((err) => console.warn("[migrate] idx_gateway_recent:", String(err).slice(0, 100)));
await sql`
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_exam_passed_recent

Copilot uses AI. Check for mistakes.
jaturapornchai added a commit that referenced this pull request May 3, 2026
…ssion, latency, RPM, breaker, fallback)

Tackled the audit list end-to-end. Two of the requested ten were already
implemented in route.ts (empty-body retry on both proxied + relaxed-retry
paths), one was deferred (undici dispatcher re-enable — Node 20 already
uses undici keep-alive and the previous nvidia-hang bug has no public fix
record), one was scoped out (Thai exam dataset is a separate task).

Speed
- #1 Client-side RPM throttle (rate-budget.ts) — Redis sliding-window per
  (provider, model). Skip locally before burning the upstream 429 budget,
  which is what triggers minutes-long cooldowns. Per-provider RPM defaults
  in free-model-catalog map documented free-tier limits (Groq 30, SEA-LION
  10, Typhoon 200, etc.). Fail-open on Redis outage.
- #2 Latency-aware sort — added SambaNova to FAST_STREAM_PROVIDERS, plus a
  PROVIDER_LATENCY_HINT_MS table so providers with no production data yet
  rank by their documented first-token latency instead of being pinned at
  9999999 (which previously stranded new providers at the back forever).

Reliability
- #4 Fallback chain — provider-diversity interleave in reorderForLatency:
  the first 6 candidates round-robin across providers, so a single-provider
  outage no longer stalls the whole chain. Tail keeps strict latency order.
- #5 Circuit breaker (3-state) — acquireCircuitProbe/releaseCircuitProbe
  in learning.ts adds explicit half-open: when fail_streak>=2 and cooldown
  has expired, only HALF_OPEN_PROBE_LIMIT concurrent probes are admitted.
  API exported; wiring into chat/completions left for the fallback-chain
  refactor follow-up.
- #6 Deprecation watcher — deprecatedAfter field on FreeModelCatalogEntry.
  getActiveFreeModelCatalog filters EOL models from /v1/models, scanner,
  search, and isHardcodedFreeModel. getModelsDeprecatingSoon emits warn
  logs from the worker scan cycle for anything within 7 days of EOL.
  Cerebras llama-3.3-70b (2026-02-16) and qwen-3-235b-a22b-instruct-2507
  (2026-05-27) tagged.

Quality
- #8 Tool-call argument JSON validation — validateToolCallArguments in
  tool-call-repair.ts. isResponseBad now rejects responses where any
  tool_call argument string fails JSON.parse or isn't a JSON object;
  caller falls back to next provider instead of returning malformed
  tool_calls to clients.
- #10 Regression warning — checkRegressionWarning() in learning.ts
  compares last-1h success rate vs prior-23h baseline per modelId; logs
  REGRESSION warning when drop >=20pp (with min sample sizes both sides).
  10-min cooldown per modelId prevents log spam. Hooked into
  recordOutcomeLearning fire-and-forget on non-quota fails.

Tests 95/95, build 0/0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants