Skip to content

feat(gateway): add uptime & latency metrics - #1146

Merged
steebchen merged 12 commits into
mainfrom
claude/routing-uptime-latency-metrics-011CV267Fqts91qog59vi3sa
Nov 20, 2025
Merged

steebchen merged 12 commits into
mainfrom
claude/routing-uptime-latency-metrics-011CV267Fqts91qog59vi3sa

Conversation

@steebchen

@steebchen steebchen commented Nov 11, 2025

Copy link
Copy Markdown
Member

Enhance provider selection logic to consider uptime and latency from the last 5 minutes in addition to price.

  • Add getProviderMetrics functions in @llmgateway/db to fetch uptime/latency from model_provider_mapping_history
  • Update getCheapestFromAvailableProviders to accept optional metrics and use weighted scoring algorithm:
    PRICE_WEIGHT = 0.2 (20%)
    UPTIME_WEIGHT = 0.5 (50%)
    LATENCY_WEIGHT = 0.3 (30%)
  • Integrate metrics into both auto routing and regular model routing in the gateway
  • Falls back to price-only selection when metrics unavailable

Summary by CodeRabbit

  • New Features

    • Provider selection now factors recent runtime performance (uptime & latency) with weighted scoring, falling back to price-only when metrics are unavailable.
    • Routing metadata about selection (available providers, chosen provider, scores/reason) is recorded with logs and persisted.
    • New public API to retrieve short-term provider–model performance metrics.
  • Tests

    • Added comprehensive tests for metrics aggregation and combination-specific retrieval.
  • Documentation

    • Expanded routing docs with Smart Routing algorithm, examples, and best practices.

Enhance provider selection logic to consider uptime and
latency from the last 5 minutes in addition to price.

- Add getProviderMetrics functions in @llmgateway/db to
  fetch uptime/latency from model_provider_mapping_history
- Update getCheapestFromAvailableProviders to accept
  optional metrics and use weighted scoring algorithm:
  * 40% price
  * 30% uptime
  * 30% latency
- Integrate metrics into both auto routing and regular
  model routing in the gateway
- Falls back to price-only selection when metrics unavailable
@github-actions github-actions Bot changed the title Add uptime and latency to routing logic feat(gateway): add uptime & latency metrics Nov 11, 2025
@coderabbitai

coderabbitai Bot commented Nov 11, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Gateway now fetches recent provider-model runtime metrics and supplies a metrics map to provider selection; selection scores providers by price, uptime and latency, filters unstable/experimental providers, and returns routing metadata that is persisted to logs and passed downstream.

Changes

Cohort / File(s) Summary
Provider Metrics DB
packages/db/src/provider-metrics.ts
New module: defines ProviderMetrics and functions getProviderMetrics(minutes?) and getProviderMetricsForCombinations(combinations, minutes?) that aggregate uptime, average latency and totalRequests over a time window and return Map<string, ProviderMetrics> keyed by modelId:providerId.
DB Public Exports
packages/db/src/index.ts
Re-exported provider-metrics with export * from "./provider-metrics.js".
DB Schema & Migrations
packages/db/src/schema.ts, packages/db/migrations/1763458310_dark_silver_fox.sql, packages/db/migrations/meta/_journal.json
Added optional routingMetadata JSON column to log table and migration/journal entries.
DB Tests
packages/db/src/provider-metrics.spec.ts
New tests validating aggregation, time-window filtering, zero-log edge cases, and combination-specific retrieval.
Provider Selection Model
packages/models/src/get-cheapest-from-available-providers.ts
Signature extended to accept metricsMap?: Map<string, ProviderMetrics> and now returns `ProviderSelectionResult
Models Tests
packages/models/src/models.spec.ts
Test assertions updated to reference cheapestProvider?.provider... reflecting new selection result shape.
Gateway Chat Integration
apps/gateway/src/chat/chat.ts
Imports getProviderMetricsForCombinations, fetches recent metrics for candidate model-provider pairs (~5 minutes), passes metricsMap into getCheapestFromAvailableProviders, and threads routing metadata into logging and downstream handlers.
Log Creation
apps/gateway/src/chat/tools/create-log-entry.ts
createLogEntry signature updated to accept optional routingMetadata?: RoutingMetadata and now includes routingMetadata in returned log entries.
Models Public Types
packages/models/src/...
New public exports: RoutingMetadata and ProviderSelectionResult types added alongside the updated getCheapestFromAvailableProviders declaration.
Docs
apps/docs/content/features/routing.mdx
Documentation updated with Smart Routing algorithm, scoring weights, epsilon-greedy note, routing metadata exposure, and expanded auto-routing guidance and examples.

Sequence Diagram(s)

sequenceDiagram
    participant Chat as Gateway Chat
    participant DB as Metrics DB
    participant Selector as Provider Selector

    Chat->>DB: getProviderMetricsForCombinations(combinations, 5)
    DB-->>Chat: metricsMap {"modelId:providerId" → ProviderMetrics}
    Chat->>Selector: getCheapestFromAvailableProviders(providers, selectedModel, metricsMap)

    rect rgb(245,252,250)
      Note over Selector: Filter unstable/experimental providers\nNormalize price, uptime, latency\nCompute composite score = 0.2*price + 0.5*(1-uptime_norm) + 0.3*latency_norm
    end

    Selector-->>Chat: ProviderSelectionResult { provider, metadata }
    Chat->>Chat: createLogEntry(..., routingMetadata = metadata)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay attention to SQL aggregation, GROUP/WHERE/time-window correctness and zero-log handling in packages/db/src/provider-metrics.ts.
  • Review normalization, weighting, stability filtering, and price-only fallback in packages/models/src/get-cheapest-from-available-providers.ts.
  • Verify typing and propagation of metricsMap and routingMetadata across apps/gateway/src/chat/chat.ts and apps/gateway/src/chat/tools/create-log-entry.ts.
  • Inspect new tests for deterministic time setup and coverage of edge cases.

Possibly related PRs

Suggested reviewers

  • smakosh

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main feature addition: integrating uptime and latency metrics into the gateway's routing logic, which aligns with the primary objective of the PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/routing-uptime-latency-metrics-011CV267Fqts91qog59vi3sa

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 436e257 and e6b614d.

📒 Files selected for processing (4)
  • apps/gateway/src/chat/chat.ts (3 hunks)
  • packages/db/src/index.ts (1 hunks)
  • packages/db/src/provider-metrics.ts (1 hunks)
  • packages/models/src/get-cheapest-from-available-providers.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Always use top-level import; never use require() or dynamic import()

Files:

  • packages/db/src/index.ts
  • packages/db/src/provider-metrics.ts
  • apps/gateway/src/chat/chat.ts
  • packages/models/src/get-cheapest-from-available-providers.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use any or as any in this TypeScript project unless absolutely necessary
Always use top-level import; do not use require or dynamic import()

Files:

  • packages/db/src/index.ts
  • packages/db/src/provider-metrics.ts
  • apps/gateway/src/chat/chat.ts
  • packages/models/src/get-cheapest-from-available-providers.ts
packages/db/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use Drizzle ORM with the latest object syntax for database schema and code

Files:

  • packages/db/src/index.ts
  • packages/db/src/provider-metrics.ts
{apps/{api,gateway}/**/*.ts,packages/db/**/*.ts}

📄 CodeRabbit inference engine (CLAUDE.md)

For read operations, use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • packages/db/src/index.ts
  • packages/db/src/provider-metrics.ts
  • apps/gateway/src/chat/chat.ts
apps/{gateway,api}/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

apps/{gateway,api}/**/*.ts: Use Hono for HTTP routing in Gateway and API services
Use Zod schemas for request/response validation in server routes

Files:

  • apps/gateway/src/chat/chat.ts
🧠 Learnings (1)
📚 Learning: 2025-10-20T21:33:03.287Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-20T21:33:03.287Z
Learning: Applies to packages/db/**/*.ts : Use Drizzle ORM with the latest object syntax for database schema and code

Applied to files:

  • packages/db/src/index.ts
  • packages/db/src/provider-metrics.ts
🧬 Code graph analysis (3)
packages/db/src/provider-metrics.ts (3)
packages/models/src/get-cheapest-from-available-providers.ts (1)
  • ProviderMetrics (4-10)
packages/db/src/db.ts (1)
  • db (21-25)
packages/db/src/schema.ts (1)
  • modelProviderMappingHistory (679-726)
apps/gateway/src/chat/chat.ts (2)
packages/db/src/provider-metrics.ts (1)
  • getProviderMetricsForCombinations (81-145)
packages/models/src/get-cheapest-from-available-providers.ts (1)
  • getCheapestFromAvailableProviders (38-155)
packages/models/src/get-cheapest-from-available-providers.ts (3)
packages/db/src/provider-metrics.ts (1)
  • ProviderMetrics (6-12)
packages/models/src/types.ts (2)
  • AvailableModelProvider (265-268)
  • ModelWithPricing (253-262)
packages/models/src/models.ts (1)
  • ProviderModelMapping (24-121)
🪛 GitHub Actions: ci
packages/models/src/get-cheapest-from-available-providers.ts

[error] 91-91: TS2339: Property 'id' does not exist on type 'ModelWithPricing'.


[error] 1-1: Command 'tsc && resolve-tspaths' failed during @llmgateway/models build.

🪛 GitHub Actions: e2e
packages/models/src/get-cheapest-from-available-providers.ts

[error] 91-91: TypeScript error TS2339: Property 'id' does not exist on type 'ModelWithPricing'.

Comment thread packages/models/src/get-cheapest-from-available-providers.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/db/src/provider-metrics.ts (1)

81-145: Consider using idiomatic Drizzle ORM syntax for the OR conditions.

The OR condition building (lines 92-97) uses raw SQL templates. For better type safety and readability, consider using Drizzle's or() and eq() functions:

+import { and, eq, gte, or, sql, sum } from "drizzle-orm";
...
-const conditions = combinations.map((combo) =>
-  and(
-    sql`${modelProviderMappingHistory.modelId} = ${combo.modelId}`,
-    sql`${modelProviderMappingHistory.providerId} = ${combo.providerId}`,
-  ),
-);
+const conditions = combinations.map((combo) =>
+  and(
+    eq(modelProviderMappingHistory.modelId, combo.modelId),
+    eq(modelProviderMappingHistory.providerId, combo.providerId),
+  ),
+);

const results = await db
  .select({...})
  .from(modelProviderMappingHistory)
  .where(
    and(
      gte(modelProviderMappingHistory.minuteTimestamp, fiveMinutesAgo),
-     sql`(${sql.join(conditions, sql` OR `)})`,
+     or(...conditions),
    ),
  )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e6b614d and 5ed24f7.

📒 Files selected for processing (2)
  • packages/db/src/provider-metrics.ts (1 hunks)
  • packages/models/src/get-cheapest-from-available-providers.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Always use top-level import; never use require() or dynamic import()

Files:

  • packages/models/src/get-cheapest-from-available-providers.ts
  • packages/db/src/provider-metrics.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use any or as any in this TypeScript project unless absolutely necessary
Always use top-level import; do not use require or dynamic import()

Files:

  • packages/models/src/get-cheapest-from-available-providers.ts
  • packages/db/src/provider-metrics.ts
packages/db/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use Drizzle ORM with the latest object syntax for database schema and code

Files:

  • packages/db/src/provider-metrics.ts
{apps/{api,gateway}/**/*.ts,packages/db/**/*.ts}

📄 CodeRabbit inference engine (CLAUDE.md)

For read operations, use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • packages/db/src/provider-metrics.ts
🧬 Code graph analysis (2)
packages/models/src/get-cheapest-from-available-providers.ts (2)
packages/models/src/types.ts (2)
  • AvailableModelProvider (265-268)
  • ModelWithPricing (253-262)
packages/models/src/models.ts (1)
  • ProviderModelMapping (24-121)
packages/db/src/provider-metrics.ts (2)
packages/db/src/db.ts (1)
  • db (21-25)
packages/db/src/schema.ts (1)
  • modelProviderMappingHistory (679-726)
🪛 ESLint
packages/db/src/provider-metrics.ts

[error] 1-1: Resolve error: EACCES: permission denied, open '/HjoUnatxrC'
at Object.writeFileSync (node:fs:2409:20)
at l (/home/jailuser/git/node_modules/.pnpm/get-tsconfig@4.10.1/node_modules/get-tsconfig/dist/index.cjs:7:13685)
at createFilesMatcher (/home/jailuser/git/node_modules/.pnpm/get-tsconfig@4.10.1/node_modules/get-tsconfig/dist/index.cjs:7:14437)
at Object.resolve (/home/jailuser/git/node_modules/.pnpm/eslint-import-resolver-typescript@4.4.4_eslint-plugin-import@2.32.0_eslint@9.34.0_jiti@2.6.1_/node_modules/eslint-import-resolver-typescript/lib/index.cjs:298:107)
at withResolver (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:180:23)
at fullResolve (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:201:22)
at relative (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:217:10)
at resolve (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:233:12)
at resolveImportType (/home/jailuser/git/node_modules/.pnpm/eslint-plugin-import@2.32.0_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1___986ec7d736a20dae59d4d473ff8a6f0d/node_modules/eslint-plugin-import/lib/core/importType.js:126:2822)
at computeRank (/home/jailuser/git/node_modules/.pnpm/eslint-plugin-import@2.32.0_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1___986ec7d736a20dae59d4d473ff8a6f0d/node_modules/eslint-plugin-import/lib/rules/order.js:529:43)

(import/order)


[error] 1-1: Resolve error: EACCES: permission denied, open '/RXwsvBEJwV'
at Object.writeFileSync (node:fs:2409:20)
at l (/home/jailuser/git/node_modules/.pnpm/get-tsconfig@4.10.1/node_modules/get-tsconfig/dist/index.cjs:7:13685)
at createFilesMatcher (/home/jailuser/git/node_modules/.pnpm/get-tsconfig@4.10.1/node_modules/get-tsconfig/dist/index.cjs:7:14437)
at Object.resolve (/home/jailuser/git/node_modules/.pnpm/eslint-import-resolver-typescript@4.4.4_eslint-plugin-import@2.32.0_eslint@9.34.0_jiti@2.6.1_/node_modules/eslint-import-resolver-typescript/lib/index.cjs:298:107)
at withResolver (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:180:23)
at fullResolve (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:201:22)
at relative (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:217:10)
at resolve (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:233:12)
at checkSourceValue (/home/jailuser/git/node_modules/.pnpm/eslint-plugin-import@2.32.0_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1___986ec7d736a20dae59d4d473ff8a6f0d/node_modules/eslint-plugin-import/lib/rules/no-useless-path-segments.js:85:53)
at checkSourceValue (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/moduleVisitor.js:32:5)

(import/no-useless-path-segments)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: generate / run
  • GitHub Check: lint / run
  • GitHub Check: test / run
  • GitHub Check: build / run
  • GitHub Check: e2e-shards (5)
  • GitHub Check: e2e-shards (2)
  • GitHub Check: e2e-shards (3)
  • GitHub Check: e2e-shards (4)
  • GitHub Check: e2e-shards (1)
  • GitHub Check: autofix
🔇 Additional comments (6)
packages/db/src/provider-metrics.ts (1)

14-71: Verify the uptime calculation logic.

The uptime calculation currently only subtracts upstreamErrorsCount from logsCount to determine successful requests (lines 35, 56). However, the schema includes other error types: errorsCount, clientErrorsCount, and gatewayErrorsCount.

Should uptime account for all error types, or is the current approach (only upstream errors) intentional? If all errors represent failures from the user's perspective, the calculation should be:

const successfulRequests = totalLogs - (totalUpstreamErrors + totalClientErrors + totalGatewayErrors);

Please clarify the intended uptime definition and verify whether other error types should be included in the calculation.

packages/models/src/get-cheapest-from-available-providers.ts (5)

12-27: LGTM: Well-designed scoring system.

The weighted scoring approach (40% price, 30% uptime, 30% latency) with reasonable default values provides a balanced provider selection mechanism while maintaining backward compatibility.


72-101: LGTM: Robust metrics integration with proper fallback.

The implementation correctly falls back to price-only selection when metrics are unavailable (lines 72-75) and properly constructs the metrics lookup key (line 91) matching the format from the db package.


103-151: LGTM: Normalization and scoring logic is mathematically sound.

The normalization correctly handles edge cases (e.g., zero range when all providers have identical values) and consistently treats lower scores as better across all metrics. The weighted combination (lines 136-139) properly applies the scoring weights.


153-181: LGTM: Clean fallback preserves original behavior.

The selectByPriceOnly helper maintains backward compatibility and properly isolates the price-only logic for when metrics are unavailable.


38-44: Type signature fix verified across all call sites.

All 5 call sites of getCheapestFromAvailableProviders pass variables that include the id property:

  • models.spec.ts (3 calls): Variables derive from the imported models array, which has confirmed id properties
  • chat.ts line 1025: selectedModel uses .id at line 1019
  • chat.ts line 1131: modelWithPricing is found from the models array and uses .id at line 1134

The intersection type ModelWithPricing & { id: string } correctly resolves the previous TypeScript error and all callers comply with the new signature.

Comment thread packages/models/src/get-cheapest-from-available-providers.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
packages/db/src/provider-metrics.spec.ts (1)

86-322: Good coverage of aggregation semantics; consider one tiny edge-case test.

The getProviderMetrics tests thoroughly exercise the main behaviors: empty data, aggregation over multiple minutes, multiple provider/model pairs, time-window filtering, zero-log exclusion, and uptime/latency extremes. The expectations line up with the implementation (using upstreamErrorsCount for failures and keying as ${modelId}:${providerId}), so this gives high confidence in the DB layer.

Minor optional improvement: the “null or zero totalDuration” case currently only tests zero; if you ever allow manual backfills that might store NULL for totalDuration despite the default, a dedicated test inserting totalDuration: null would directly assert that the Number(row.totalDuration) || 0 coercion behaves as intended.

packages/models/src/get-cheapest-from-available-providers.ts (3)

3-21: Weights and default metrics are reasonable but might deserve configurability.

Importing ProviderMetrics from @llmgateway/db removes the prior type duplication, and the ProviderScore shape + weight constants make the scoring criteria explicit. The 0.4/0.3/0.3 split and defaults of 95% uptime / 1000 ms latency seem sensible as an initial heuristic.

Design-wise, two optional thoughts:

  • You might eventually want these weights and defaults to be configurable (env or config file), so tuning routing behavior doesn’t require a deploy.
  • Treating missing metrics as “95% / 1000 ms” can make providers with no telemetry look quite good compared to ones with merely decent real metrics; if you’d rather be conservative, consider slightly penalizing unknown metrics instead of assuming they’re solid.

65-144: Metrics-based scoring and price-only fallback are coherent; watch behavior for missing per-provider metrics.

The control flow is sound: if metricsMap is absent or empty you delegate to selectByPriceOnly, otherwise you:

  • compute a per-provider price (reusing the existing discount-aware formula),
  • look up metrics under ${modelWithPricing.id}:${provider.providerId},
  • normalize price (cheaper → lower score), uptime (higher → lower score), and latency (lower → lower score),
  • and select the provider with the lowest weighted score.

A couple of nuanced behaviors to be aware of:

  • When some stable providers lack entries in metricsMap, they get DEFAULT_UPTIME/DEFAULT_LATENCY. This can advantage “unknown” providers over ones with modest but real metrics (e.g., uptime 80% vs default 95%). If that’s intentional, great; otherwise you might want to bias defaults slightly worse than typical telemetry or even treat missing metrics as a pure price-only dimension.
  • When metricsMap has no entries for the current model but isn’t empty (e.g., metrics for other models), all providers share the same default uptime/latency, so the normalization naturally collapses to a price-only ordering, which matches the intended fallback.

Overall the scoring math (ranges, normalization, and tie-breaking via the first element) is consistent and should behave predictably.


146-173: Price-only fallback preserves existing behavior; consider deduplicating the pricing formula.

selectByPriceOnly correctly mirrors the original logic: it iterates the stable providers, computes the average of input/output prices with discount applied, and returns the cheapest. This keeps the no-metrics path backward compatible.

You now have the same pricing calculation in both the main scoring loop and this helper. If you plan to tweak pricing in the future (e.g., weight input vs output differently or add request-level pricing), extracting a small computeEffectivePrice(modelWithPricing, providerId) helper would avoid divergence between the metrics and price-only paths.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5ed24f7 and 7cc650b.

📒 Files selected for processing (2)
  • packages/db/src/provider-metrics.spec.ts (1 hunks)
  • packages/models/src/get-cheapest-from-available-providers.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/db/src/provider-metrics.spec.ts (3)
packages/db/src/db.ts (1)
  • db (21-25)
packages/db/src/schema.ts (4)
  • modelProviderMappingHistory (679-726)
  • modelProviderMapping (624-677)
  • model (593-622)
  • provider (559-591)
packages/db/src/provider-metrics.ts (2)
  • getProviderMetrics (25-71)
  • getProviderMetricsForCombinations (81-145)
packages/models/src/get-cheapest-from-available-providers.ts (3)
packages/models/src/types.ts (2)
  • AvailableModelProvider (265-268)
  • ModelWithPricing (253-262)
packages/db/src/provider-metrics.ts (1)
  • ProviderMetrics (6-12)
packages/models/src/models.ts (1)
  • ProviderModelMapping (24-121)
🪛 ESLint
packages/db/src/provider-metrics.spec.ts

[error] 1-1: Resolve error: EACCES: permission denied, open '/rVTiAJkuqz'
at Object.writeFileSync (node:fs:2409:20)
at l (/home/jailuser/git/node_modules/.pnpm/get-tsconfig@4.10.1/node_modules/get-tsconfig/dist/index.cjs:7:13685)
at createFilesMatcher (/home/jailuser/git/node_modules/.pnpm/get-tsconfig@4.10.1/node_modules/get-tsconfig/dist/index.cjs:7:14437)
at Object.resolve (/home/jailuser/git/node_modules/.pnpm/eslint-import-resolver-typescript@4.4.4_eslint-plugin-import@2.32.0_eslint@9.34.0_jiti@2.6.1_/node_modules/eslint-import-resolver-typescript/lib/index.cjs:298:107)
at withResolver (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:180:23)
at fullResolve (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:201:22)
at relative (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:217:10)
at resolve (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:233:12)
at resolveImportType (/home/jailuser/git/node_modules/.pnpm/eslint-plugin-import@2.32.0_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1___986ec7d736a20dae59d4d473ff8a6f0d/node_modules/eslint-plugin-import/lib/core/importType.js:126:2822)
at computeRank (/home/jailuser/git/node_modules/.pnpm/eslint-plugin-import@2.32.0_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1___986ec7d736a20dae59d4d473ff8a6f0d/node_modules/eslint-plugin-import/lib/rules/order.js:529:43)

(import/order)


[error] 1-1: Resolve error: EACCES: permission denied, open '/azxrkKKmom'
at Object.writeFileSync (node:fs:2409:20)
at l (/home/jailuser/git/node_modules/.pnpm/get-tsconfig@4.10.1/node_modules/get-tsconfig/dist/index.cjs:7:13685)
at createFilesMatcher (/home/jailuser/git/node_modules/.pnpm/get-tsconfig@4.10.1/node_modules/get-tsconfig/dist/index.cjs:7:14437)
at Object.resolve (/home/jailuser/git/node_modules/.pnpm/eslint-import-resolver-typescript@4.4.4_eslint-plugin-import@2.32.0_eslint@9.34.0_jiti@2.6.1_/node_modules/eslint-import-resolver-typescript/lib/index.cjs:298:107)
at withResolver (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:180:23)
at fullResolve (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:201:22)
at relative (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:217:10)
at resolve (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/resolve.js:233:12)
at checkSourceValue (/home/jailuser/git/node_modules/.pnpm/eslint-plugin-import@2.32.0_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1___986ec7d736a20dae59d4d473ff8a6f0d/node_modules/eslint-plugin-import/lib/rules/no-useless-path-segments.js:85:53)
at checkSourceValue (/home/jailuser/git/node_modules/.pnpm/eslint-module-utils@2.12.1_@typescript-eslint+parser@8.39.1_eslint@9.34.0_jiti@2.6.1__t_eb0afb446ca3f59399f5d681f0059e64/node_modules/eslint-module-utils/moduleVisitor.js:32:5)

(import/no-useless-path-segments)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: test / run
  • GitHub Check: lint / run
  • GitHub Check: build / run
  • GitHub Check: generate / run
  • GitHub Check: e2e-shards (5)
  • GitHub Check: e2e-shards (2)
  • GitHub Check: autofix
  • GitHub Check: e2e-shards (3)
  • GitHub Check: e2e-shards (4)
  • GitHub Check: e2e-shards (1)
🔇 Additional comments (3)
packages/db/src/provider-metrics.spec.ts (2)

17-25: Test setup and time control look coherent; just confirm timer behavior.

The DB cleanup + seeding in beforeEach correctly respects FK constraints (history → mappings → model → provider) and gives each test an isolated, deterministic state. Using vi.setSystemTime(mockDate) plus static timestamps makes the “last N minutes” filters reproducible, and vi.useRealTimers() in afterEach restores the environment.

One thing to verify: depending on your Vitest configuration, you may need vi.useFakeTimers() before vi.setSystemTime for Date.now()-based code to be fully controlled. If your existing suite already relies on this pattern and passes consistently, you’re fine; otherwise, consider adding vi.useFakeTimers() in beforeEach to avoid any environment-dependent flakiness.

Also applies to: 82-84


324-559: Combination-specific metrics tests are solid and match the query behavior.

The getProviderMetricsForCombinations suite cleanly validates all key behaviors: empty combinations, selective inclusion of only requested pairs, multi-combination aggregation, custom time windows, dropping combinations with no data or zero logs, and correctness of uptime/latency/totalRequests for partial failures and the “single combination” path.

These tests closely mirror the query contract in provider-metrics.ts (per-combination OR conditions, same ${modelId}:${providerId} keying, same aggregation formula), so the higher-level routing code can safely rely on these results.

packages/models/src/get-cheapest-from-available-providers.ts (1)

31-63: Signature and stability filtering look correct; just ensure the model id contract is clear.

Requiring modelWithPricing: ModelWithPricing & { id: string } addresses the earlier TS issue and gives you a concrete identifier for metrics lookup. The stability filter correctly excludes unstable and experimental providers by preferring provider-level stability over model-level stability, while treating missing stability as acceptable.

One thing to verify across the stack: modelWithPricing.id must match the modelId stored in model_provider_mapping_history (used as the key in the DB metrics map). If any caller passes a different identifier (e.g., a display name), metrics lookups will silently fall back to the default uptime/latency instead of using real data.

steebchen and others added 2 commits November 18, 2025 00:14
- Fix uptime calculation to use all errors instead of only upstream errors
- Adjust routing weights to prioritize uptime (50%) over price (20%)
- Add routing metadata to logs for debugging routing decisions
- Fix price display in routing metadata to show full precision

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/db/src/provider-metrics.ts (1)

92-97: Prefer Drizzle's eq() helper over raw SQL templates for safety.

The raw sql template mixing column references and values is harder to read and more error-prone. Drizzle's eq() helper provides better type safety and clarity.

Apply this diff:

+import { and, eq, gte, sql, sum } from "drizzle-orm";

 	// Build OR conditions for each combination
 	const conditions = combinations.map((combo) =>
 		and(
-			sql`${modelProviderMappingHistory.modelId} = ${combo.modelId}`,
-			sql`${modelProviderMappingHistory.providerId} = ${combo.providerId}`,
+			eq(modelProviderMappingHistory.modelId, combo.modelId),
+			eq(modelProviderMappingHistory.providerId, combo.providerId),
 		),
 	);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7cc650b and 4d1862f.

📒 Files selected for processing (6)
  • apps/gateway/src/chat/chat.ts (15 hunks)
  • apps/gateway/src/chat/tools/create-log-entry.ts (3 hunks)
  • packages/db/src/provider-metrics.ts (1 hunks)
  • packages/db/src/schema.ts (1 hunks)
  • packages/models/src/get-cheapest-from-available-providers.ts (3 hunks)
  • packages/models/src/models.spec.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
packages/db/src/provider-metrics.ts (2)
packages/db/src/db.ts (1)
  • db (21-25)
packages/db/src/schema.ts (1)
  • modelProviderMappingHistory (692-739)
apps/gateway/src/chat/tools/create-log-entry.ts (1)
packages/models/src/get-cheapest-from-available-providers.ts (1)
  • RoutingMetadata (23-34)
apps/gateway/src/chat/chat.ts (2)
packages/models/src/get-cheapest-from-available-providers.ts (2)
  • RoutingMetadata (23-34)
  • getCheapestFromAvailableProviders (50-180)
packages/db/src/provider-metrics.ts (1)
  • getProviderMetricsForCombinations (81-145)
packages/models/src/get-cheapest-from-available-providers.ts (3)
packages/models/src/types.ts (2)
  • AvailableModelProvider (265-268)
  • ModelWithPricing (253-262)
packages/db/src/provider-metrics.ts (1)
  • ProviderMetrics (6-12)
packages/models/src/models.ts (1)
  • ProviderModelMapping (25-122)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: e2e-shards (3)
  • GitHub Check: e2e-shards (5)
  • GitHub Check: e2e-shards (4)
  • GitHub Check: e2e-shards (2)
  • GitHub Check: e2e-shards (1)
  • GitHub Check: test / run
  • GitHub Check: generate / run
  • GitHub Check: lint / run
  • GitHub Check: build / run
  • GitHub Check: autofix
🔇 Additional comments (8)
packages/models/src/models.spec.ts (1)

530-537: LGTM! Test assertions correctly updated for new return type.

The test assertions now correctly access cheapestProvider?.provider.providerId to match the new ProviderSelectionResult<T> return type from getCheapestFromAvailableProviders.

apps/gateway/src/chat/tools/create-log-entry.ts (1)

4-4: LGTM! Routing metadata properly threaded through log entries.

The routingMetadata parameter is cleanly added and propagated through the log entry creation flow, enabling downstream tracking of provider selection decisions.

Also applies to: 32-32, 65-65

packages/db/src/schema.ts (1)

411-422: LGTM! Schema properly extended with routing metadata.

The routingMetadata field correctly mirrors the RoutingMetadata interface structure, enabling persistence of provider selection decisions and scores.

packages/db/src/provider-metrics.ts (1)

25-71: LGTM! Metrics calculation logic is sound.

Both functions correctly:

  • Aggregate logs, errors, and duration over the time window
  • Calculate uptime as (successful requests / total requests) * 100
  • Calculate average latency as totalDuration / totalRequests
  • Skip entries with zero logs to avoid division by zero

The use of Map with "modelId:providerId" keys enables efficient lookups in the routing logic.

Also applies to: 81-145

apps/gateway/src/chat/chat.ts (1)

21-21: LGTM! Metrics integration is clean and consistent.

The routing logic correctly:

  • Fetches metrics for relevant provider combinations before selection
  • Passes the metricsMap to getCheapestFromAvailableProviders
  • Extracts routing metadata from the result
  • Propagates metadata through log entries

The pattern is consistently applied in both auto-routing and regular routing paths.

Also applies to: 44-44, 689-689, 1024-1043, 1131-1150

packages/models/src/get-cheapest-from-available-providers.ts (3)

57-82: Good practice: filtering unstable and experimental providers.

The stability filtering at the beginning of the function ensures that only production-ready providers are considered for routing, reducing the risk of routing to unreliable endpoints.


129-160: Normalization and scoring logic is correctly implemented.

The normalized scoring correctly:

  • Maps each metric to 0-1 range where lower is better
  • Handles edge cases where all providers have the same metric value (range = 0)
  • Applies weighted combination with lower scores preferred
  • Selects the provider with the minimum composite score

The logic is sound and will properly balance price, uptime, and latency (once the weights are corrected).


182-231: Clean fallback preserves backward compatibility.

The selectByPriceOnly helper maintains the original price-based selection behavior when metrics are unavailable, while still returning the new ProviderSelectionResult structure with routing metadata. The placeholder score: 0 is acceptable for the price-only case.

Comment thread packages/models/src/get-cheapest-from-available-providers.ts
Comment thread packages/models/src/get-cheapest-from-available-providers.ts
steebchen and others added 4 commits November 18, 2025 17:31
…ction

Added a mechanism to randomly select a provider 1% of the time to ensure all providers receive periodic traffic and build up metrics.
…blem

- Add 1% random exploration rate to ensure all providers get traffic
- Disable exploration during tests to maintain deterministic behavior
- Fix price precision display in routing metadata

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/models/src/get-cheapest-from-available-providers.ts (2)

87-107: Epsilon‑greedy exploration behavior is global; consider making rate configurable

The 1% exploration happens for all non‑test environments, regardless of whether metrics are present, and returns "random-exploration" with empty providerScores. That matches the stated goal of spreading traffic but does introduce non‑determinism in prod.

If you foresee needing finer control (e.g., temporarily disabling exploration or tuning the rate per environment), consider driving EXPLORATION_RATE from an env var (with 0.01 as the default) and optionally populating providerScores even on exploration to keep metadata shape consistent.


206-251: Handling of providers without explicit pricing could be tightened (currently treated as free)

In selectByPriceOnly, if a ProviderModelMapping is missing or has no inputPrice/outputPrice, totalPrice becomes 0, making that provider always “cheapest”. This mirrors the previous behavior but is semantically odd for an unpriced provider.

If this is not intentional, you could:

  • Skip providers with no pricing, or
  • Treat missing prices as Infinity (or a large sentinel) rather than 0.

For example:

-    const totalPrice =
-      (((providerInfo?.inputPrice || 0) + (providerInfo?.outputPrice || 0)) / 2) *
-      discountMultiplier;
+    if (!providerInfo || (providerInfo.inputPrice == null && providerInfo.outputPrice == null)) {
+      continue; // or set totalPrice = Number.POSITIVE_INFINITY
+    }
+
+    const totalPrice =
+      (((providerInfo.inputPrice || 0) + (providerInfo.outputPrice || 0)) / 2) *
+      discountMultiplier;

This would keep the routing logic aligned with real pricing data and avoid unintentionally favoring unpriced mappings.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d1862f and d506d5f.

📒 Files selected for processing (4)
  • packages/db/migrations/1763458310_dark_silver_fox.sql (1 hunks)
  • packages/db/migrations/meta/_journal.json (1 hunks)
  • packages/models/src/get-cheapest-from-available-providers.ts (3 hunks)
  • packages/models/src/models.spec.ts (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/db/migrations/1763458310_dark_silver_fox.sql
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/models/src/models.spec.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/models/src/get-cheapest-from-available-providers.ts (3)
packages/models/src/types.ts (2)
  • AvailableModelProvider (265-268)
  • ModelWithPricing (253-262)
packages/db/src/provider-metrics.ts (1)
  • ProviderMetrics (6-12)
packages/models/src/models.ts (1)
  • ProviderModelMapping (25-122)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: e2e-shards (5)
  • GitHub Check: build / run
  • GitHub Check: test / run
  • GitHub Check: generate / run
  • GitHub Check: lint / run
  • GitHub Check: e2e-shards (3)
  • GitHub Check: e2e-shards (4)
  • GitHub Check: e2e-shards (2)
  • GitHub Check: e2e-shards (1)
  • GitHub Check: autofix
🔇 Additional comments (3)
packages/models/src/get-cheapest-from-available-providers.ts (2)

3-42: Types and routing metadata shape look consistent with the metrics/logging design

ProviderScore, RoutingMetadata, and ProviderSelectionResult align cleanly with ProviderMetrics from @llmgateway/db and the described routing_metadata log schema (providerId, score, uptime, latency, price). Shapes are coherent and should serialize cleanly downstream.


110-201: Metrics scoring and normalization logic looks sound and degrades to price‑only when metrics are missing

The scoring path:

  • Computes per‑provider price (including discount) once and builds providerScores.
  • Uses min/max normalization for price, uptime, and latency, with sane handling when ranges collapse to 0.
  • Correctly treats lower price, higher uptime, and lower latency as better by flipping uptime and latency in the normalized scores.
  • Uses the weights (0.2 price / 0.5 uptime / 0.3 latency) consistently in the final composite score, and picks the min‑score provider.

When metricsMap has entries but none match the current providers, all providers end up with the same default uptime/latency, so uptime/latency terms cancel out and the choice reduces effectively to price ranking, which is exactly the intended fallback behavior.

Overall this path looks correct and robust for edge cases (single provider, partially missing metrics, etc.).

packages/db/migrations/meta/_journal.json (1)

454-466: New migration journal entry is well‑formed and ordered

The added entry for idx 65 follows the existing structure (version/tag/when/breakpoints) and preserves the monotonic idx ordering, so it should integrate cleanly with the migration tooling.

Updated the routing documentation to reflect improvements in the smart routing algorithm, including a weighted scoring system and epsilon-greedy exploration to address the cold start problem. Added details on routing metadata for better transparency in provider selection.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/docs/content/features/routing.mdx (1)

197-204: Update "Coming Soon" section to reflect already-implemented features.

Based on code verification:

Already implemented (should be moved to "Current Implementation" section):

  • Performance-based routing: Fully implemented in get-cheapest-from-available-providers.ts with weighted scoring (50% uptime, 30% latency, 20% price) based on historical ProviderMetrics data
  • Tool call optimization: Auto-routing in chat.ts (lines 976-987) automatically filters to models supporting tools when tools are specified in the request

Genuinely "Coming Soon" (can remain):

  • Content-aware routing (no message content analysis currently)
  • Multi-model orchestration (no evidence of combining multiple models)

Move the two implemented features to your "Current Implementation" section and revise the "Coming Soon" section to remove them.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d506d5f and 07922f6.

📒 Files selected for processing (1)
  • apps/docs/content/features/routing.mdx (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: test / run
  • GitHub Check: build / run
  • GitHub Check: lint / run
  • GitHub Check: e2e-shards (4)
  • GitHub Check: e2e-shards (3)
  • GitHub Check: e2e-shards (5)
  • GitHub Check: e2e-shards (1)
  • GitHub Check: e2e-shards (2)
  • GitHub Check: autofix
🔇 Additional comments (4)
apps/docs/content/features/routing.mdx (4)

78-100: Provider-Specific Routing updates are clear and well-documented.

The clarification about "without any fallbacks," concrete examples, and the note explaining that explicit provider routing bypasses smart routing are all helpful additions that accurately describe the expected behavior.


102-124: Auto routing documentation accurately reflects implemented capabilities—no changes needed.

The verification confirms all three "Current Implementation" features are fully implemented in the codebase:

  • Cost-effective model selection: Confirmed via active use of getCheapestFromAvailableProviders() and getCheapestModelForProvider() functions throughout the routing logic
  • Automatic scaling to more powerful models: Confirmed via context size estimation (apps/gateway/src/chat/chat.ts:835–836) and capability-based filtering of suitable providers (apps/gateway/src/chat/chat.ts:951–952)
  • Large context handling: Confirmed via hardcoded model pool (gpt-oss-120b, gpt-5-nano, gpt-4.1-nano) with context-aware selection and filter options for free models and reasoning capabilities

All "Coming Soon" features are correctly identified as not yet implemented. The auto routing system is production-ready.


218-235: Auto routing recommendation for production is appropriate—verification confirms adequate stability and edge case handling.

The codebase demonstrates production readiness:

  • Hardcoded allowed models (gpt-oss-120b, gpt-5-nano, gpt-4.1-nano) prevent unpredictable behavior
  • Explicit error handling: HTTP 400 responses when constraints are violated (no free models available, no non-reasoning models available)
  • Fallback strategy: Defaults to gpt-5-nano/openai when suitable models exhaust
  • Parameterized filtering: free_models_only and no_reasoning options properly filter provider capabilities
  • Cost optimization: Selects cheapest available provider from filtered pool
  • E2E test coverage: api-individual.e2e.ts tests auto routing with reasoning_effort scenarios

The guidance in the Best Practices section correctly positions auto routing for production use with confidence.


31-74: All technical specifications in the documentation are verified as accurate.

The weight percentages (50% uptime, 30% latency, 20% price), epsilon-greedy exploration rate (1%), and metrics time window (5 minutes) all match the code implementation exactly. The routing metadata is properly defined in the interface, returned from the routing function, and passed through the request lifecycle for logging.

Comment on lines +54 to +62
**Routing Metadata**:

Every request includes detailed routing metadata in the logs, showing:

- Available providers that were considered
- Selected provider and selection reason
- Scores for each provider (including uptime, latency, and price)

This transparency allows you to understand and debug routing decisions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Verify routing metadata is actually logged and available to users.

The documentation states that routing metadata (available providers, selected provider and reason, provider scores) is included in request logs. Confirm that this logging is implemented and accessible to users through the standard logging/observability channels.


🏁 Script executed:

#!/bin/bash
# Description: Verify routing metadata is logged

# Search for logging of routing decisions
rg -n 'routing.*metadata|routingMetadata|provider.*score|selected.*provider' --type ts --type js -C 3

# Search for log statements related to provider selection
rg -n 'log.*provider|logger.*routing|console.*routing' --type ts --type js -C 2 | head -20

Length of output: 30769



🏁 Script executed:

#!/bin/bash
# Check for logging API endpoints that might expose routing metadata to users

# Search for API endpoints that might return logs with routing metadata
rg -n 'logs|getLogs|getLog|fetchLogs|routingMetadata' --type ts -C 2 \
  | grep -E '(route|handler|endpoint|get.*logs|post.*logs)' \
  | head -30

# Search for any public API or dashboard features related to logs
fd -e ts -e tsx -e js | xargs grep -l "log.*route\|logs.*api\|getLog" | head -10

Length of output: 2213


🏁 Script executed:

#!/bin/bash
# Search for documentation or API documentation about accessing logs
find . -type f \( -name "*.md" -o -name "*.mdx" \) | xargs grep -l "log\|routing.*metadata" 2>/dev/null | head -10

Length of output: 395


🏁 Script executed:

#!/bin/bash
# Search for API endpoints or features that expose logs to users
rg -n 'logs|getLog|fetchLog' --type ts -C 2 | grep -E 'export|function|route|handler' | head -40

# Look for log retrieval endpoints in gateway
fd -type f -name "*.ts" | xargs grep -l "logs" --include="*.ts" | grep -E 'gateway|route|api'

Length of output: 3007



🏁 Script executed:

#!/bin/bash
# Examine the logs route implementation to see what it returns
cat apps/api/src/routes/logs.ts | head -100

Length of output: 2862


🏁 Script executed:

#!/bin/bash
# Check what fields are returned in the logs response
rg -A 30 'export.*logs|function logs' apps/api/src/routes/logs.ts | head -60

Length of output: 1029


🏁 Script executed:

#!/bin/bash
# Check the test to see what fields are expected in the response
cat apps/api/src/routes/logs.spec.ts | grep -A 20 "expect(json"

Length of output: 3724



🏁 Script executed:

#!/bin/bash
# Get the complete logSchema definition to see all fields
rg -A 100 'const logSchema = z.object' apps/api/src/routes/logs.ts | head -120

Length of output: 3315


🏁 Script executed:

#!/bin/bash
# Search explicitly for routingMetadata in logs.ts
rg -n 'routingMetadata' apps/api/src/routes/logs.ts

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Get full logs.ts file to see the complete schema and endpoint
wc -l apps/api/src/routes/logs.ts
tail -n 200 apps/api/src/routes/logs.ts

Length of output: 4691


routing metadata is not exposed in the logs API response

The documentation claims routing metadata is included in request logs for user visibility, but verification shows the routingMetadata field is stored in the database but not included in the /logs API schema. The logSchema at apps/api/src/routes/logs.ts returns many fields (requestId, usedProvider, cost, customHeaders, source, etc.) but omits routingMetadata entirely, meaning users cannot actually access this data through the logs API to debug routing decisions as documented.

Either add routingMetadata to the logSchema response at apps/api/src/routes/logs.ts lines 28-59, or update the documentation to remove claims about routing metadata transparency.

🤖 Prompt for AI Agents
In apps/api/src/routes/logs.ts around lines 28-59 the logs API response schema
omits the stored routingMetadata field so clients cannot access routing decision
details; add routingMetadata to the logSchema response (and any TypeScript
types/serializers used there) and map it from the DB record into the response
payload—define its shape (e.g., availableProviders array, selectedProvider,
selectionReason, scores object) or reuse an existing interface, and ensure any
validation/serialization middleware includes it; alternatively, if exposing it
is not desired, update apps/docs/content/features/routing.mdx to remove the
claim that routing metadata is available via the /logs API.

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