Skip to content

Add providers_used + llm_duration_ms to llm_usage_logs - #8

Merged
emooreatx merged 1 commit into
mainfrom
feat/usage-provider-attribution-and-llm-latency
Sep 6, 2026
Merged

Add providers_used + llm_duration_ms to llm_usage_logs#8
emooreatx merged 1 commit into
mainfrom
feat/usage-provider-attribution-and-llm-latency

Conversation

@emooreatx

Copy link
Copy Markdown
Contributor

What

Adds two columns to llm_usage_logs (migration 0021) plus the model/schema/route wiring:

  • providers_used — which upstream actually served the calls
  • llm_duration_ms — summed LLM-call time, distinct from the existing interaction wall-clock

Why

Both closed blind spots found while diagnosing a user-visible latency incident on 2026-08-28.

Provider attribution. We record models_used but not the serving provider. On OpenRouter one model id (qwen/qwen3.6-35b-a3b) is served by ~10 providers. Measured live that day, identical 192-token output:

provider tok/s provider tok/s
Io Net 170.2 SiliconFlow 51.2
Venice 145.1 AkashML 36.7
CoreWeave 109.5 DeepInfra 22.0
Phala 102.3
Parasail 91.5

A 7.7x spread behind one model id. With no attribution, "the model is slow" can't be narrowed to "this provider is slow" from data — the culprit had to be found by benchmarking all ten by hand out-of-band.

True latency. duration_ms is measured start → finalization, and finalization fires on the stale timeout (5 min) or a call-limit trip, not when the last LLM call returns. So it includes agent-side and idle time. A 24-minute duration_ms row sent us hunting for a violated request_timeout: 120 that was never actually violated. The per-call sum already existed in the proxy's accumulator and was discarded at send time; it's now reported separately.

Compatibility

  • Both columns nullable/defaulted → existing rows stay valid
  • Both request fields optional, and the schema ignores unknown fields → proxy and billing can deploy in either order (verified both directions)

Spock

Migration follows the Issue #5 convention: DDL wrapped in spock.replicate_ddl() to fan out over the ddl_sql replication set, plain-DDL fallback for non-Spock (CI/local/single-node), and ADD COLUMN IF NOT EXISTS so it's idempotent on a node that already received the DDL via replication.

Producer side: CIRISProxy 6f2f640.

⚠️ Not merged directly — please review deploy.yml first

I deliberately did not push to main. deploy.yml triggers on push to main and deploys straight to production with a hardcoded GOOGLE_CLIENT_IDS (line 88) containing 2 client IDs, while production currently runs 4:

live in prod (US + EU):          deploy.yml would set:
  ...vsrucm66hl39jei1f5f20kb49om8t9pb   ...vsrucm66hl39jei1f5f20kb49om8t9pb
  ...l421ndojcs5nm7lkln53jj29kf7kck91   ...l421ndojcs5nm7lkln53jj29kf7kck91
  ...vqfv6ecjgc1ku7n6bm4hllg6csdiaild   (dropped)
  ...760hu05lq7onca4iq9rtl49v9gho0pdc   (dropped — desktop client)

Merging as-is would drop two client IDs and break OAuth for those apps. That drift is pre-existing and independent of this PR, but it makes any merge to main hazardous until fixed.

🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ceb0d3264e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +105 to +107
DROP TABLE IF EXISTS stripe_payment_intents;
DROP INDEX IF EXISTS idx_admin_oauth_sessions_expires_at;
DROP TABLE IF EXISTS admin_oauth_sessions;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve reconciled tables when downgrading revision 0020

Downgrading from 0020 to its declared down-revision 0019 should restore the schema produced by 0019, where both of these tables already exist. Instead, this drops the tables and all their data while leaving revisions 0018/0019 recorded as applied; downgrading farther also makes 0019's unconditional op.drop_table() fail. Because 0020 only reconciles pre-existing objects across peers, its downgrade must not remove those objects.

Useful? React with 👍 / 👎.

Comment on lines +92 to +94
else:
# Non-Spock environment (CI, local dev, single-node) — plain DDL.
bind.execute(sa.text(_ENSURE_DDL))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Split migration 0020 fallback DDL into separate executions

In a non-Spock environment using the repository's configured postgresql+asyncpg URL, this passes the five statements in _ENSURE_DDL to SQLAlchemy as one prepared statement. The asyncpg dialect rejects multiple commands in a prepared statement, so a fresh local or single-node alembic upgrade head fails at 0020 instead of creating the tables. Execute each DDL statement separately on the fallback path.

Useful? React with 👍 / 👎.

Comment on lines +79 to +81
else:
# Non-Spock environment (CI, local dev, single-node) — plain DDL.
bind.execute(sa.text(ddl))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Split migration 0021 fallback DDL into separate executions

When Spock is absent and the configured asyncpg driver is used, _UPGRADE_DDL and _DOWNGRADE_DDL each contain two commands but are submitted through one SQLAlchemy execution. Asyncpg cannot prepare multiple commands at once, so migration 0021 fails in local, CI-like, or fresh-region databases on both upgrade and downgrade. Split the DDL and execute each ALTER TABLE independently.

Useful? React with 👍 / 👎.

Comment thread app/db/models.py
# Upstream provider(s) that actually served the calls. On OpenRouter one
# model id maps to ~10 providers with a 7.7x throughput spread, so
# models_used alone cannot attribute slowness. See migration 0021.
providers_used: Mapped[list[str]] = mapped_column(ARRAY(String), nullable=False, default=list)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Expose captured diagnostics through interaction analytics

Although the new values are persisted, the repository's detailed analytics path, get_interaction_margins, still selects and serializes only models_used and duration_ms; neither providers_used nor llm_duration_ms appears in InteractionMarginResponse. Operators using the admin analytics API therefore cannot perform the provider/latency diagnosis this change is intended to enable without direct database access. Add both columns to that query and response model.

Useful? React with 👍 / 👎.

@emooreatx

Copy link
Copy Markdown
Contributor Author

Correction to the warning in this PR's description.

I said merging would drop two client ids from production and break OAuth. That was wrong — I checked the wrong source of truth before writing it.

deploy.yml targets 149.28.120.73 (the legacy box), not the bridge nodes. The production bridge nodes get GOOGLE_CLIENT_IDS from CIRISBridge's Ansible vault via roles/billing/templates/env.j2:40, and I've since verified all 4 ids are live on both 108.61.242.236 and 46.224.81.217. This workflow does not touch them, so merging would not have broken OAuth there.

The hardcoding was still real drift, and while fixing it I found something more serious in the same file: the deploy accepted any SSH host key for that IP (ssh-keyscan + StrictHostKeyChecking=no), and that host's key has in fact changed while its health endpoint no longer responds.

Both are addressed in #9, which should land first. This PR is otherwise unchanged and ready.

@emooreatx
emooreatx force-pushed the feat/usage-provider-attribution-and-llm-latency branch from ceb0d32 to b9be970 Compare August 28, 2026 13:35
Closes two telemetry blind spots found during a user-visible latency
incident on 2026-08-28.

providers_used: we record models_used but not which upstream actually
served the call. On OpenRouter a single model id is served by ~10
providers whose measured throughput spans 22-170 tok/s (7.7x). Without
attribution "the model is slow" cannot be narrowed to "this provider is
slow" from data — we had to benchmark all ten by hand out-of-band.

llm_duration_ms: the existing duration_ms is interaction wall-clock,
start -> finalization, where finalization fires on the stale timeout
(5 min) or a call-limit trip rather than when the last LLM call returns.
It therefore includes agent-side and idle time. A 24-minute duration_ms
row sent us hunting for a violated request_timeout: 120 that was never
violated. llm_duration_ms is the summed LLM-call time only, so the two
can be compared instead of conflated.

Migration 0021 follows the Issue #5 convention: DDL is wrapped in
spock.replicate_ddl() so it fans out over the ddl_sql replication set,
with a plain-DDL fallback for non-Spock environments, and uses
ADD COLUMN IF NOT EXISTS so it is idempotent on a node that already
received the DDL via replication.

Both columns are nullable/defaulted and both request fields are
optional, so old rows stay valid and a proxy that does not yet send
them keeps working.
@emooreatx
emooreatx force-pushed the feat/usage-provider-attribution-and-llm-latency branch from b9be970 to 2c36d03 Compare September 6, 2026 22:12
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@sonarqubecloud

sonarqubecloud Bot commented Sep 6, 2026

Copy link
Copy Markdown

@emooreatx
emooreatx merged commit f566d30 into main Sep 6, 2026
5 checks passed
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.

1 participant