Skip to content

Make token estimate ratio configurable (#50) - #100

Merged
leynos merged 32 commits into
mainfrom
issue-50-configurable-estimate-token-count-char-ratio
May 31, 2026
Merged

Make token estimate ratio configurable (#50)#100
leynos merged 32 commits into
mainfrom
issue-50-configurable-estimate-token-count-char-ratio

Conversation

@lodyai

@lodyai lodyai Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

Summary

This branch makes the OpenAI-compatible adapter's preflight token estimate configurable so operators can tune the characters-per-token ratio for models whose tokenizer shape differs from the default heuristic.

Closes #50.

Review walkthrough

Validation

  • make check-fmt: passed.
  • make lint: passed.
  • make typecheck: passed.
  • make markdownlint: passed.
  • make nixie: passed.
  • make test PYTEST_XDIST_WORKERS=1: OpenAI adapter coverage passed; the full suite hit unrelated pytest-timeout setup errors in rotating py-pglite-backed async fixture tests.
  • make test PYTEST_XDIST_WORKERS=0: OpenAI adapter coverage passed; the full suite hit one unrelated pytest-timeout setup error in a py-pglite-backed async fixture test.
  • Isolated reruns passed for each timed-out test: tests/canonical_storage/test_tei_headers.py::test_tei_header_large_raw_xml_round_trip_uses_compressed_storage, tests/canonical_storage/test_workflow_checkpoints.py::test_checkpoint_store_persists_across_unit_of_work, tests/test_ingestion_integration.py::test_ingest_multi_source_preserves_all_sources, and tests/canonical_storage/test_episodes.py::test_episode_get_remains_compatible_with_legacy_uncompressed_rows.

Notes

The full-suite timeouts occurred in unrelated async fixture setup while the event loop was waiting in selectors.EpollSelector.select. The affected tests changed between runs and passed when isolated, while the OpenAI adapter tests consistently passed.

Summary by Sourcery

Make the OpenAI-compatible adapter’s preflight token estimation use a configurable characters-per-token ratio and validate it via adapter config.

New Features:

  • Allow OpenAI-compatible adapters to configure a chars_per_token ratio used for preflight token budget estimation.

Enhancements:

  • Validate chars_per_token as a positive value in OpenAI-compatible adapter configuration and thread it through adapter construction.
  • Extend tests to cover configurable preflight token budget behavior and invalid chars_per_token values.
  • Document the configurable preflight token estimation ratio in the user guide.

Documentation:

  • Update the user guide to describe configurable chars_per_token for OpenAI-compatible preflight token estimates.

Tests:

  • Add budget tests ensuring preflight estimation respects the configured chars_per_token ratio.
  • Extend configuration validation tests and fixtures to cover chars_per_token handling.

@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c814c2fe-6657-47a4-8286-8124292aa68f

📥 Commits

Reviewing files that changed from the base of the PR and between 55446ad and 70c7a89.

📒 Files selected for processing (3)
  • docs/developers-guide.md
  • docs/users-guide.md
  • episodic/llm/openai_api/utils.py

Walkthrough

Thread configurable chars_per_token into adapter config and heuristics; split the adapter into episodic.llm.openai_api submodules with a facade; add preflight and post-response token-budget enforcement, structured diagnostic logs, tests, snapshots, fixtures, and docs.

Changes

Configurable chars-per-token estimation

Layer / File(s) Summary
Public facade and package layout
episodic/llm/openai_adapter.py, episodic/llm/openai_api/__init__.py, pyproject.toml, episodic/llm/__init__.py
Convert openai_adapter.py to a facade re-exporting adapter/config and helpers from episodic.llm.openai_api; add package docstring describing internal responsibilities; update architecture policy and narrow episodic.llm exports.
Adapter core and HTTP retry
episodic/llm/openai_api/adapter.py
Add OpenAICompatibleLLMConfig dataclass and OpenAICompatibleLLMAdapter with eager validation, optional preflight checks in generate(), Tenacity-backed _send_with_retries, _send_once, timeout/auth wiring, and resource cleanup.
Request payload and response handling
episodic/llm/openai_api/request.py, episodic/llm/openai_api/response.py
Add _coerce_operation, _path_for_operation, and _build_payload for chat/responses; classify HTTP statuses (retryable 429,500,502–504; redirects 300–399 as errors), decode JSON and normalise payloads to LLMResponse, mapping adapter validation errors to LLMProviderResponseError.
Token estimation and budget enforcement
episodic/llm/openai_api/utils.py, tests/test_llm_openai_adapter_budgets.py
Implement _estimate_token_count(chars_per_token, ...) using ceil(len(...)/chars_per_token) across prompt parts; add preflight checks and response usage enforcement that log structured events and raise LLMTokenBudgetExceededError on overrun; validate chars_per_token finiteness and positivity.
Test fixtures and observability
tests/fixtures/llm.py, openai_test_types.py
Add _OpenAIAdapterLogSpy protocol and openai_log_spy fixture that captures structured adapter ERROR events; extend invalid-config builder and adapter factory to accept chars_per_token overrides (default 4.0).
Token estimation and budget validation tests
tests/test_llm_openai_adapter_budgets.py, tests/__snapshots__/test_llm_openai_adapter_budgets.ambr
Add Hypothesis property-test for _estimate_token_count; async preflight rejection tests including non-default chars_per_token=2.0; parametrised usage-budget tests with mock transports; snapshot assertions for structured budget-rejection logs.
Config tests and diagnostics snapshot
tests/test_llm_openai_adapter_config.py, tests/__snapshots__/test_llm_openai_adapter_config.ambr
Extend invalid-config parameter matrix for chars_per_token edge cases (0, negative, NaN, inf, wrong type) and add snapshot-backed test asserting openai_adapter.config_rejected diagnostic payload is emitted on rejection.
Retries and status handling tests
tests/test_llm_openai_adapter_retries.py
Add tests asserting HTTP 302 redirect responses and malformed JSON bytes are treated as provider response errors.
BDD mock server and steps
tests/features/llm_adapter.feature, tests/steps/test_llm_adapter_steps.py
Extend mock server to support fail_first, centralise server start/run helpers and generation runner, and add a BDD scenario that verifies configured chars_per_token affects token estimation and succeeds on first attempt when no transient failures occur.
User and developer documentation
docs/users-guide.md, docs/developers-guide.md
Document OpenAICompatibleLLMConfig(chars_per_token=...) constraints (finite, > 0), calibration guidance comparing prompt chars to provider input-token usage, and expand developers guide with two-phase budget enforcement, ceil(len(prompt_text)/chars_per_token) preflight formula, tuning instructions, and openai_api module layout.
Test infra fixes
tests/fixtures/database.py, tests/workflow_test_utils.py
Dispose SQLAlchemy engines from pglite managers; deterministically discover an ephemeral artifact-server port by binding a temporary socket and returning the chosen port.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Adapter as OpenAICompatibleLLMAdapter
  participant Utils as openai_api.utils
  participant Retry as _send_with_retries
  participant Provider as LLM Provider (HTTP)

  Client->>Adapter: generate(LLMRequest)
  Adapter->>Utils: _estimate_token_count(chars_per_token, prompt)
  Utils-->>Adapter: estimated_input_tokens / validation
  Adapter->>Retry: _send_with_retries(request_payload)
  Retry->>Provider: HTTP POST /chat/completions or /responses
  Provider-->>Retry: HTTP response (status + body)
  Retry-->>Adapter: provider JSON payload
  Adapter->>Utils: validate response usage and shape
  Utils-->>Adapter: usage validation result (or raise)
  Adapter-->>Client: LLMResponse (or raise)
Loading

Possibly related issues

Poem

Set the ratio, count the chars,
Log the breach where budgets sparse.
Split the adapter, tests align,
Docs and snapshots mark the sign.
🎉

🚥 Pre-merge checks | ✅ 18 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Observability ⚠️ Warning Structured logging at decision boundaries is present, but usage_budget_exceeded logs omit chars_per_token field; no metrics or distributed tracing added for budget/retry changes. Add chars_per_token to usage_budget_exceeded logs; add latency/budget-rejection metrics; add distributed tracing spans at HTTP boundaries.
Architectural Complexity And Maintainability ⚠️ Warning Package split into openai_api/ adds ~400 net lines, no reuse outside adapter boundary; domain port segregation is good architecture but unrelated to chars_per_token feature. Separate feature-only PR from architecture-cleanup PR. The split needs reuse justification or measurable reduction in call-site complexity to warrant four-module indirection.
✅ Passed checks (18 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Make token estimate ratio configurable (#50)' directly aligns with the PR's primary change, which adds a configurable chars_per_token field to OpenAICompatibleLLMConfig for preflight token estimation, and includes the required issue reference.
Description check ✅ Passed The PR description comprehensively covers the changeset, including a clear summary of the core feature, detailed review walkthrough of key files, validation results, and references to the linked issue #50.
Linked Issues check ✅ Passed The implementation fully satisfies issue #50 requirements: OpenAICompatibleLLMConfig exposes chars_per_token (default 4.0), validation rejects non-finite/non-positive values, _estimate_token_count uses the configured ratio, and tests exercise non-default ratios and invalid vectors.
Out of Scope Changes check ✅ Passed All substantive changes map to the stated objective of making token estimation configurable. Database fixture disposal and ephemeral port selection changes serve the supporting test infrastructure for the OpenAI adapter functionality.
Docstring Coverage ✅ Passed Docstring coverage is 92.78% which is sufficient. The required threshold is 80.00%.
Testing (Overall) ✅ Passed Validation rejects NaN/infinity/zero, Hypothesis tests ceiling-division semantics across 100 examples, preflight budget test fails if ratio ignored, BDD test confirms real HTTP behaviour change.
User-Facing Documentation ✅ Passed Docs/users-guide.md comprehensively documents new chars_per_token feature with examples, constraints, and calibration guidance. Breaking import-path change signposted via migration note.
Developer Documentation ✅ Passed Developers guide documents new LLM adapter boundary and package layout; users guide covers configurable char ratio; architecture boundaries updated in pyproject.toml; internal APIs documented.
Module-Level Documentation ✅ Passed All Python modules in the PR carry module-level docstrings clearly explaining purpose, utility and relationships to other components.
Testing (Unit And Behavioural) ✅ Passed Unit tests validate config and error paths. Property-based tests exercise token estimation. Integration tests verify budget enforcement. BDD scenario tests HTTP adapter workflow.
Testing (Property / Proof) ✅ Passed Hypothesis property testing added for _estimate_token_count invariants. Parametrised tests cover invalid chars_per_token values: 0, negative, NaN, infinity, type errors.
Testing (Compile-Time / Ui) ✅ Passed Snapshot tests capture meaningful structured error events with deterministic fields, parametrised scenario variants, semantic error assertions, stable JSON, no nondeterministic values.
Unit Architecture ✅ Passed Proper separation of queries, commands, side-effects. Dependencies injected at boundaries. Components composable with clear responsibilities. Tests verify at proper boundaries with snapshots.
Domain Architecture ✅ Passed Configurable chars_per_token is isolated to adapter implementation; domain contracts remain unmodified; adapter exports are removed from domain package.
Security And Privacy ✅ Passed Credentials properly guarded: test keys fake only, API keys never logged, validation uses math.isfinite(), safe JSON/URL handling, adapter symbols removed from public module.
Performance And Resource Use ✅ Passed Token estimation O(1); config validation fixed-size loop; retries bounded by max_attempts; timeouts set; clients lifecycle-managed; payloads bounded by input text.
Concurrency And State ✅ Passed ContextVar-based log override provides safe per-test isolation; no fire-and-forget tasks, no locks held across awaits, and AsyncClient lifecycle is properly managed with clear ownership semantics.
Rust Compiler Lint Integrity ✅ Passed This is a Python project with no Rust code. The custom check for Rust compiler lint integrity does not apply to Python codebases.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📋 Issue Planner

Built with CodeRabbit's Coding Plans for faster development and fewer bugs.

View plan used: #50

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-50-configurable-estimate-token-count-char-ratio

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

@sourcery-ai

sourcery-ai Bot commented May 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a configurable chars-per-token ratio to the OpenAI-compatible LLM adapter and threads it through config, validation, preflight token estimation, fixtures, tests, and documentation so operators can tune budget preflight behavior per adapter.

Sequence diagram for generate preflight budget using chars_per_token

sequenceDiagram
actor User
participant OpenAICompatibleLLMAdapter
participant LLMRequest
participant LLMTokenBudget
participant _validate_preflight_budget
participant _estimate_token_count

User->>OpenAICompatibleLLMAdapter: generate(request)
OpenAICompatibleLLMAdapter->>LLMRequest: token_budget
LLMRequest-->>OpenAICompatibleLLMAdapter: token_budget
OpenAICompatibleLLMAdapter->>_validate_preflight_budget: request, token_budget, _chars_per_token
_validate_preflight_budget->>_estimate_token_count: chars_per_token, system_prompt, prompt
_estimate_token_count-->>_validate_preflight_budget: estimated_input_tokens
_validate_preflight_budget-->>OpenAICompatibleLLMAdapter: return
OpenAICompatibleLLMAdapter-->>User: LLMResponse
Loading

File-Level Changes

Change Details Files
Make preflight token estimation use a configurable chars-per-token ratio from adapter configuration.
  • Change _estimate_token_count to accept a chars_per_token argument and compute ceil(len(combined) / chars_per_token) instead of hard-coded divisor 4.
  • Update _validate_preflight_budget to accept chars_per_token and pass it through to _estimate_token_count when estimating input tokens.
  • Store chars_per_token from OpenAICompatibleLLMConfig on OpenAICompatibleLLMAdapter and use it in generate() when performing preflight budget validation.
episodic/llm/openai_adapter.py
Extend OpenAI-compatible LLM configuration and fixtures to support chars_per_token with validation and wiring.
  • Add chars_per_token: float = 4.0 field to OpenAICompatibleLLMConfig with a default matching the previous heuristic.
  • Extend _validate_llm_config to assert chars_per_token is greater than zero and include it in the list of validated fields.
  • Update test fixtures to allow passing chars_per_token through _build_invalid_config and _build_adapter, including defaulting and type-casting for this new field.
episodic/llm/openai_adapter.py
tests/fixtures/llm.py
Add and update tests to cover chars_per_token configuration, validation, and budget behavior.
  • Add a budget test that configures chars_per_token=2.0 and verifies that preflight estimation rejects a prompt that would only fail under the stricter ratio, confirming the ratio is honored.
  • Extend configuration validation tests to treat chars_per_token <= 0 as invalid, with explicit parametrized cases for zero and negative values.
tests/test_llm_openai_adapter_budgets.py
tests/test_llm_openai_adapter_config.py
Document the new configurable chars_per_token ratio for OpenAI-compatible preflight token estimates.
  • Add a bullet to the user guide noting that OpenAI-compatible preflight token estimates can tune the configured chars_per_token ratio away from the default four-characters-per-token heuristic.
docs/users-guide.md

Assessment against linked issues

Issue Objective Addressed Explanation
#50 Expose an optional chars_per_token field on OpenAICompatibleLLMConfig (default 4.0) and validate that it is positive in _validate_llm_config.
#50 Update _estimate_token_count and preflight budget validation to use the configured chars_per_token value instead of a hard-coded 4.
#50 Maintain existing test behavior and add tests that exercise a non-default chars_per_token ratio.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

codescene-delta-analysis[bot]

This comment was marked as outdated.

@lodyai
lodyai Bot force-pushed the issue-50-configurable-estimate-token-count-char-ratio branch from 2d784bc to adc7690 Compare May 15, 2026 11:49
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
leynos marked this pull request as ready for review May 15, 2026 14:16

@sourcery-ai sourcery-ai 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.

Sorry @leynos, you have reached your weekly rate limit of 2500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@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: adc7690ff7

ℹ️ 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 thread episodic/llm/openai_adapter.py Outdated
@coderabbitai coderabbitai Bot added the Issue label May 15, 2026

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@episodic/llm/openai_adapter.py`:
- Line 240: The validation for config.chars_per_token currently only checks >0
and therefore permits NaN/Infinity; update the predicate in the validation tuple
that mentions config.chars_per_token to also call math.isfinite (e.g., change
the tuple from checking config.chars_per_token <= 0 to checking not
math.isfinite(config.chars_per_token) or config.chars_per_token <= 0 and update
the error message to "chars_per_token must be a finite number greater than
zero."); also add parametrised unit tests for the preflight validation (the same
test that covers 0 and -1.0) to include float('nan') and float('inf') to prevent
regressions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 713d3cee-8bd6-4a8b-bb3d-6b524dc802d2

📥 Commits

Reviewing files that changed from the base of the PR and between c58825d and adc7690.

📒 Files selected for processing (5)
  • docs/users-guide.md
  • episodic/llm/openai_adapter.py
  • tests/fixtures/llm.py
  • tests/test_llm_openai_adapter_budgets.py
  • tests/test_llm_openai_adapter_config.py

Comment thread episodic/llm/openai_adapter.py Outdated
codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented May 22, 2026

Copy link
Copy Markdown
Owner

@coderabbitai Have the following failed checks now been resolved?

If further work is required, please provide an AI agent prompt for the remaining work to be done to address these failures.

Do not treat warnings as optional or aspirational. Where a change is out of scope for this PR, propose a GitHub issue unless one exists already.

❌ Failed checks (4 errors, 6 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Testing (Overall) ❌ Error Functional test is sound but config validation omits NaN and infinity edge cases, which cause runtime failures or disable budget enforcement. Add test cases for float('nan') and float('inf'). Use math.isfinite() in validation at line 240 alongside positivity check.
Module-Level Documentation ❌ Error Module docstrings present but insufficient. One-liners lack detail on utility, function and relationships. episodic/llm/openai_adapter.py needs substantive documentation. Expand docstrings to document module purpose, main responsibilities, utility/functions provided and key relationships to other components. Use narrative or structured formats instead of single sentences.
Unit Architecture ❌ Error Validation omits non-finite values (NaN, infinity), allowing runtime failures instead of explicit early rejection. This hides fallible work behind apparently pure computation. Use math.isfinite() in _validate_llm_config before the positivity check to reject NaN and infinity. Add test vectors for float('nan') and float('inf') to prevent regression.
Security And Privacy ❌ Error Validation of chars_per_token accepts NaN (crashes at runtime) and inf (bypasses budget enforcement). Review comment flagged this with a patch. Extend validation to use math.isfinite() alongside positivity check. Add tests for float('nan') and float('inf') to prevent regression.
User-Facing Documentation ⚠️ Warning Documentation mentions chars_per_token is configurable but lacks essential guidance. Operators cannot determine how to actually use this feature based on the documentation provided. Add concrete guidance: how to configure chars_per_token in OpenAICompatibleLLMConfig, typical values for different models, how to determine correct ratios, and configuration examples.
Testing (Unit And Behavioural) ⚠️ Warning Test coverage omits edge cases for chars_per_token validation. Only 0 and -1.0 tested; missing float('nan') and float('inf') which cause runtime errors. Extend _validate_llm_config to use math.isfinite() alongside positivity check. Add parametrised tests for float('nan') and float('inf') edge cases to prevent regression.
Testing (Property / Proof) ⚠️ Warning Configurable chars_per_token creates invariants over unbounded input space. Tests only verify boundaries, missing property coverage. Validation lacks isfinite() allowing NaN/Infinity failures. Add property tests with Hypothesis for positive finite chars_per_token range. Harden validation with math.isfinite() to reject NaN/Infinity alongside positivity check.
Domain Architecture ⚠️ Warning Validation accepts non-finite values (NaN, Infinity) in chars_per_token. NaN causes runtime ValueError; Infinity silently collapses estimates to zero, bypassing budget checks. Use math.isfinite() alongside positivity check at line 240. Add test cases for float('nan') and float('inf') to prevent regression.
Observability ⚠️ Warning PR introduces operational behaviour change without adequate observability: no logging at validation boundaries, error messages lack configuration context, non-finite values unguarded. Add logging for configuration and budget errors. Validate with math.isfinite(). Include chars_per_token value in error messages.
Concurrency And State ⚠️ Warning Validation omits non-finite checks (NaN, infinity). NaN causes ValueError; infinity collapses token estimates to zero, bypassing budget enforcement. Add math.isfinite() check alongside positivity: reject where not math.isfinite(config.chars_per_token) or config.chars_per_token <= 0. Add tests for float('nan') and float('inf').

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the current state of all relevant files
echo "=== episodic/llm/openai_adapter.py ==="
cat episodic/llm/openai_adapter.py

echo ""
echo "=== tests/test_llm_openai_adapter_config.py ==="
cat tests/test_llm_openai_adapter_config.py

echo ""
echo "=== tests/test_llm_openai_adapter_budgets.py ==="
cat tests/test_llm_openai_adapter_budgets.py

echo ""
echo "=== docs/users-guide.md (chars_per_token section) ==="
grep -n -A 20 "chars_per_token" docs/users-guide.md || echo "No chars_per_token mention found"

echo ""
echo "=== tests/fixtures/llm.py ==="
cat tests/fixtures/llm.py

Length of output: 31003


🏁 Script executed:

#!/bin/bash
# Search for existing issues related to observability/logging
gh issue list --repo leynos/episodic --state open --search "logging observability" --json number,title,url
gh issue list --repo leynos/episodic --state open --search "adapter logging" --json number,title,url

Length of output: 616


🏁 Script executed:

#!/bin/bash
gh issue create \
  --repo leynos/episodic \
  --title "Add logging and metrics to episodic/llm/openai_adapter.py validation and budget failure points" \
  --body "## Background

PR `#100` introduced a configurable \`chars_per_token\` field on \`OpenAICompatibleLLMConfig\`. A code-review observability check flagged that the adapter does not emit any structured log output at validation or budget failure boundaries, and that error messages should carry the operational configuration context needed for diagnosis in production.

## Required work

- Emit an \`ERROR\`-level log at each \`ValueError\` raised by \`_validate_llm_config\`, including the offending field name and value.
- Emit an \`ERROR\`-level log when \`_validate_preflight_budget\` raises \`LLMTokenBudgetExceededError\`, including the estimated token count, the configured budget limit, and the \`chars_per_token\` value in use.
- Emit an \`ERROR\`-level log when \`_validate_usage_budget\` raises \`LLMTokenBudgetExceededError\`, including provider-reported usage counts and budget limits.
- Consider adding metrics counters (e.g., a counter for preflight rejections and post-call budget violations) consistent with the pattern established in `#75`, `#84`, and `#93`.

## Context

- Observability gap identified during review of PR `#100` (https://github.com/leynos/episodic/pull/100).
- Related logging issues: `#75`, `#84`, `#93`.
- Requested by `@leynos`." \
  --assignee leynos

Length of output: 46


@leynos: All checks have been resolved on the current branch, with one exception that is out of scope for this PR. Here is the full status:

Check Status Notes
Testing (Overall) ✅ Resolved math.isfinite() guard added; float('nan') and float('inf') parametrised test cases present
Module-Level Documentation ✅ Resolved Multi-paragraph module docstring now documents purpose, responsibilities, retry/budget/normalisation concerns, and the configurable heuristic
Unit Architecture ✅ Resolved not math.isfinite(config.chars_per_token) or config.chars_per_token <= 0 guard in _validate_llm_config
Security And Privacy ✅ Resolved math.isfinite() rejects NaN and infinity; regression tests present
User-Facing Documentation ✅ Resolved docs/users-guide.md now includes how to configure the field, an example value (2.0), calibration guidance, and the finite-positive constraint
Testing (Unit And Behavioural) ✅ Resolved float("nan") and float("inf") are parametrised test vectors in test_llm_openai_adapter_config.py
Testing (Property / Proof) ✅ Resolved Hypothesis property test test_estimate_token_count_matches_ceiling_ratio covers the positive-finite range with 100 examples
Domain Architecture ✅ Resolved math.isfinite() check present alongside positivity check
Observability ⚠️ Partially resolved Error messages now include the chars_per_token value; however, no logging calls exist at validation or budget failure boundaries. This is out of scope for this PR — issue #110 has been opened to track it
Concurrency And State ✅ Resolved math.isfinite() guard rejects non-finite values before any arithmetic

The one remaining gap — structured log emission at _validate_llm_config, _validate_preflight_budget, and _validate_usage_budget failure points — has been captured in #110, consistent with the pattern established in #75, #84, and #93.

@leynos

leynos commented May 25, 2026

Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

codescene-delta-analysis[bot]

This comment was marked as 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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/developers-guide.md`:
- Around line 762-767: The paragraph describing
OpenAICompatibleLLMConfig(chars_per_token=...) is too long for the 80-column
rule; rewrap the Markdown paragraph and any bullet lines so no line exceeds 80
columns while preserving the original wording and inline code (chars_per_token,
ceil(len(prompt_text) / chars_per_token), etc.), breaking sentences into shorter
lines and keeping the same meaning and formatting.
- Line 762: The docs use the hyphenated form "pre-flight" which is inconsistent
with codebase terminology; update the occurrence in the sentence containing
OpenAICompatibleLLMConfig(chars_per_token=...) to use "preflight" (one word)
instead of "pre-flight" so it matches PR summary and established technical usage
in the repo.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 55f90282-ba12-4c62-9421-dfd02ada1d35

📥 Commits

Reviewing files that changed from the base of the PR and between a81c65c and 4766c08.

📒 Files selected for processing (1)
  • docs/developers-guide.md

Comment thread docs/developers-guide.md Outdated
Comment thread docs/developers-guide.md Outdated
codescene-delta-analysis[bot]

This comment was marked as 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: 2

♻️ Duplicate comments (1)
docs/developers-guide.md (1)

762-767: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Rewrap the paragraph to comply with the 80-column limit.

Lines 763 and 765 exceed 80 columns. Markdown paragraphs and bullet points must be wrapped at 80 columns.

Triage: [type:syntax/md]

Proposed fix
-- `OpenAICompatibleLLMConfig(chars_per_token=...)` controls the preflight
-  estimate. The value defaults to `4.0`, must be finite and greater than zero,
-  and is applied as `ceil(len(prompt_text) / chars_per_token)` across the
-  request prompt and optional system prompt. Tune it by comparing sampled prompt
-  character counts with provider-reported input-token usage for the target
-  model and prompt shape.
+- `OpenAICompatibleLLMConfig(chars_per_token=...)` controls the preflight
+  estimate. The value defaults to `4.0`, must be finite and greater than
+  zero, and is applied as `ceil(len(prompt_text) / chars_per_token)` across
+  the request prompt and optional system prompt. Tune it by comparing sampled
+  prompt character counts with provider-reported input-token usage for the
+  target model and prompt shape.
🤖 Prompt for AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/developers-guide.md` at lines 762-767, rewrap the bullet point
describing OpenAICompatibleLLMConfig(chars_per_token=...) so that no line
exceeds 80 columns while preserving inline code formatting (backticks around
chars_per_token, 4.0, ceil(len(prompt_text) / chars_per_token)) and the
original wording.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/developers-guide.md` around lines 762 - 767, Rewrap the markdown
paragraph that documents `OpenAICompatibleLLMConfig(chars_per_token=...)` so no
line exceeds 80 columns while preserving inline code formatting; specifically
break the sentence about the default value (`4.0`), validity (finite and > 0),
and the application `ceil(len(prompt_text) / chars_per_token)` across the
request prompt and optional system prompt into shorter lines (and keep the
guidance about tuning by comparing sampled prompt character counts with
provider-reported input-token usage intact) so each line is ≤80 characters.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@episodic/llm/openai_adapter.py`:
- Line 14: Remove the file-level "# pylint: disable=too-many-lines" from
episodic/llm/openai_adapter.py and refactor the module into smaller files so no
file exceeds 400 logical lines: extract the OpenAIAdapter class (or similarly
named adapter) into its own file, move request-building logic (e.g.
_build_request or build_request) into a separate api/request module, move
response-parsing logic (e.g. _parse_response or parse_response) into a separate
api/response module, and move utility helpers (e.g. retry/formatting/validation
functions) into a utils module; update imports so openai_adapter.py becomes a
thin facade that composes OpenAIClient/OpenAIAdapter with the new modules and
remove the blanket lint suppression.
- Around line 55-60: Refactor the _operation_label function to use Python
structural pattern matching: replace the isinstance-based branching with a
match/case on the input value so it explicitly handles None (return "default"),
LLMProviderOperation (return value.value), and str (return the string);
reference the LLMProviderOperation type in the match patterns and preserve the
current return semantics for all three cases and any fallback.

---

Duplicate comments:
In `@docs/developers-guide.md`:
- Around line 762-767: Rewrap the markdown paragraph that documents
`OpenAICompatibleLLMConfig(chars_per_token=...)` so no line exceeds 80 columns
while preserving inline code formatting; specifically break the sentence about
the default value (`4.0`), validity (finite and > 0), and the application
`ceil(len(prompt_text) / chars_per_token)` across the request prompt and
optional system prompt into shorter lines (and keep the guidance about tuning by
comparing sampled prompt character counts with provider-reported input-token
usage intact) so each line is ≤80 characters.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e57106ee-7f57-4617-b6be-80aa3a9dd5ad

📥 Commits

Reviewing files that changed from the base of the PR and between 4766c08 and 178e348.

📒 Files selected for processing (5)
  • docs/developers-guide.md
  • episodic/llm/openai_adapter.py
  • tests/fixtures/llm.py
  • tests/test_llm_openai_adapter_budgets.py
  • tests/test_llm_openai_adapter_config.py

Comment thread episodic/llm/openai_adapter.py Outdated
Comment thread episodic/llm/openai_adapter.py Outdated
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented May 26, 2026

Copy link
Copy Markdown
Owner

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response.

episodic/llm/openai_api/utils.py

Comment on file

"""Validation, estimation, and logging helpers for the OpenAI adapter."""

❌ New issue: Overall Code Complexity
This module has a mean cyclomatic complexity of 4.13 across 8 functions. The mean complexity threshold is 4

@leynos

leynos commented May 26, 2026

Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

This comment was marked as resolved.

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@episodic/llm/openai_api/adapter.py`:
- Line 3: Remove the line "from __future__ import annotations" from the top of
episodic/llm/openai_api/adapter.py; since the project targets Python >=3.14,
postponed evaluation is default, so delete this import statement (search for the
exact "from __future__ import annotations" line in adapter.py and remove it).

In `@episodic/llm/openai_api/response.py`:
- Line 3: Remove the redundant import `from __future__ import annotations` from
the module episodic.llm.openai_api.response by deleting that import line; since
the project targets Python >=3.14, deferred annotations are implicit, so simply
remove the `from __future__ import annotations` statement to eliminate the
unnecessary import.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 00b4b442-db76-407a-9213-d5e86de36316

📥 Commits

Reviewing files that changed from the base of the PR and between 178e348 and 08d8c5d.

📒 Files selected for processing (7)
  • docs/developers-guide.md
  • episodic/llm/openai_adapter.py
  • episodic/llm/openai_api/__init__.py
  • episodic/llm/openai_api/adapter.py
  • episodic/llm/openai_api/request.py
  • episodic/llm/openai_api/response.py
  • episodic/llm/openai_api/utils.py

Comment thread episodic/llm/openai_api/adapter.py Outdated
Comment thread episodic/llm/openai_api/response.py Outdated
codescene-delta-analysis[bot]

This comment was marked as outdated.

Switch `_log_error_event` to consult a `contextvars.ContextVar` for
log output redirection, and replace the `monkeypatch.setattr` fixture
with a ContextVar-based `openai_log_spy`. This removes the shared
mutable state that required `@pytest.mark.xdist_group` serialisation
across budget and config test modules.

Remove the now-unnecessary `xdist_group` markers from both
`tests/test_llm_openai_adapter_budgets.py` and
`tests/test_llm_openai_adapter_config.py`.
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented May 30, 2026

Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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.

Caution

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

⚠️ Outside diff range comments (1)
tests/steps/test_llm_adapter_steps.py (1)

332-343: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make the custom-ratio BDD scenario discriminative.

Line 339 sets a permissive budget, so the step passes whether chars_per_token is applied or ignored. Assert boundary behaviour that can only pass when the configured ratio is actually used.

🧪 Patch sketch to make the scenario prove ratio wiring
+import math
@@
 def adapter_generates_custom_ratio(
@@
-    """Generate text with chars_per_token=2.0 over real HTTP."""
+    """Generate text with a ratio-sensitive preflight budget over real HTTP."""
+    combined_chars = (
+        len(typ.cast("RenderedPrompt", context.guardrail_prompt).text)
+        + len(typ.cast("RenderedPrompt", context.rendered_prompt).text)
+    )
+    ratio = 8.0
+    max_input_tokens = math.ceil(combined_chars / ratio)
     _run_generate(
         _function_scoped_runner,
         context,
         _GenerateOptions(
             max_attempts=1,
-            chars_per_token=2.0,
+            chars_per_token=ratio,
             token_budget=LLMTokenBudget(
-                max_input_tokens=2000,
+                max_input_tokens=max_input_tokens,
                 max_output_tokens=200,
-                max_total_tokens=2200,
+                max_total_tokens=max_input_tokens + 200,
             ),
         ),
     )
@@
 def assert_generated_first_attempt(context: LLMAdapterContext) -> None:
@@
-    assert context.generated_text == "BDD generated episode draft.", (
-        "adapter should return generated text when chars_per_token=2.0 is configured"
-    )
+    assert context.generated_text == "BDD generated episode draft.", (
+        "adapter should honour configured chars_per_token at a tight preflight boundary"
+    )

Also applies to: 348-356

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/steps/test_llm_adapter_steps.py` around lines 332 - 343, The BDD
scenario is too permissive: update the _GenerateOptions/LLMTokenBudget used in
the chars_per_token test so the token budget is tight enough to fail if
chars_per_token is ignored; specifically reduce max_output_tokens (or
max_total_tokens) in the LLMTokenBudget passed to _run_generate in the
chars_per_token=2.0 case so only a response that respects the 2.0
chars-per-token ratio can fit, and make the analogous change for the second
scenario around the other call (lines 348-356) so both tests assert boundary
behaviour that proves the ratio is actually applied (refer to _GenerateOptions,
chars_per_token, and LLMTokenBudget to locate and adjust).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tests/steps/test_llm_adapter_steps.py`:
- Around line 332-343: The BDD scenario is too permissive: update the
_GenerateOptions/LLMTokenBudget used in the chars_per_token test so the token
budget is tight enough to fail if chars_per_token is ignored; specifically
reduce max_output_tokens (or max_total_tokens) in the LLMTokenBudget passed to
_run_generate in the chars_per_token=2.0 case so only a response that respects
the 2.0 chars-per-token ratio can fit, and make the analogous change for the
second scenario around the other call (lines 348-356) so both tests assert
boundary behaviour that proves the ratio is actually applied (refer to
_GenerateOptions, chars_per_token, and LLMTokenBudget to locate and adjust).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 095cda74-459b-4986-a9ab-f524b05854df

📥 Commits

Reviewing files that changed from the base of the PR and between 2296308 and 0f6d1b1.

📒 Files selected for processing (5)
  • episodic/llm/openai_api/utils.py
  • tests/fixtures/llm.py
  • tests/steps/test_llm_adapter_steps.py
  • tests/test_llm_openai_adapter_budgets.py
  • tests/test_llm_openai_adapter_config.py
💤 Files with no reviewable changes (2)
  • tests/test_llm_openai_adapter_config.py
  • tests/test_llm_openai_adapter_budgets.py

@leynos

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

…50)

Reduce max_input_tokens and max_total_tokens in both BDD scenarios
so the preflight budget checks are meaningfully exercised rather
than passing with 4x+ headroom. The tighter budgets prove that
chars_per_token is actually applied during adapter.generate().

Note: chars_per_token=2.0 produces a higher (stricter) token estimate
than the default 4.0, so a budget that passes at 2.0 inherently
passes at 4.0. The tightening makes both budgets proportional to
their respective estimates rather than attempting an impossible
cross-ratio discrimination.
codescene-delta-analysis[bot]

This comment was marked as outdated.

Pull OpenAI adapter classes out of the public `episodic.llm` re-exports
so the domain port namespace exposes only port contracts. Update four
call sites to import from `episodic.llm.openai_adapter` directly.

`episodic.llm.__init__` now exports only the 11 port-contract symbols
defined in `episodic.llm.ports`.
codescene-delta-analysis[bot]

This comment was marked as outdated.

Emit an `openai_adapter.retries_exhausted` log event when Tenacity
retries are exhausted, including the `max_attempts` and the last error
type, so operators can observe retry exhaustion from structured logs.
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented May 30, 2026

Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== episodic/llm/__init__.py ==="
cat episodic/llm/__init__.py

echo ""
echo "=== episodic/llm/openai_api/adapter.py (_send_with_retries section) ==="
grep -n "_send_with_retries\|RetryError\|_log_error_event\|retries_exhausted\|LLMTransientProviderError" episodic/llm/openai_api/adapter.py

echo ""
echo "=== Open GitHub issues ==="
gh issue list --repo leynos/episodic --state open --json number,title,url | head -30

Length of output: 4929



@leynos: All three warnings are now resolved.

Check Status Notes
Domain Architecture ✅ Resolved episodic/llm/__init__.py exports only LLM* domain-port contracts; OpenAICompatibleLLMConfig and all adapter symbols have been removed from its __all__.
Observability ✅ Resolved _send_with_retries now calls _log_error_event("openai_adapter.retries_exhausted", ...) with max_attempts and last_error_type before raising LLMTransientProviderError.
Architectural Complexity And Maintainability 🔖 Out of scope #118 remains open to track documentation of intended future consumers of episodic.llm.openai_api internal modules.

@leynos

leynos commented May 30, 2026

Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@leynos

leynos commented May 30, 2026

Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Add explicit logging side-effect notes to OpenAI adapter validation helper
Docstrings, so their command-style responsibilities are visible at the call
site.

Document the user-facing migration path for OpenAI adapter imports and clarify
why the internal OpenAI API package stays split by request, response,
validation, and adapter responsibilities.
@leynos

leynos commented May 31, 2026

Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@leynos

leynos commented May 31, 2026

Copy link
Copy Markdown
Owner

@coderabbitai please create a new GitHub issue for the following:

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Observability ⚠️ Warning Structured logging at decision boundaries is present, but usage_budget_exceeded logs omit chars_per_token field; no metrics or distributed tracing added for budget/retry changes. Add chars_per_token to usage_budget_exceeded logs; add latency/budget-rejection metrics; add distributed tracing spans at HTTP boundaries.

@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh issue create \
  --repo leynos/episodic \
  --title "Observability: add chars_per_token to usage_budget_exceeded logs, budget-rejection metrics, and HTTP tracing spans" \
  --body "## Background

PR `#100` (https://github.com/leynos/episodic/pull/100) introduced structured logging at configuration-validation and budget-rejection decision boundaries. A subsequent observability review identified three remaining gaps.

## Required work

### 1. Add \`chars_per_token\` to \`openai_adapter.usage_budget_exceeded\` log events

The \`_validate_usage_budget\` helper in \`episodic/llm/openai_api/utils.py\` currently emits \`openai_adapter.usage_budget_exceeded\` events without including the \`chars_per_token\` value that was used during preflight estimation. Add \`chars_per_token\` as a field to those log events so operators can correlate the configured ratio with observed budget overruns.

### 2. Add latency and budget-rejection metrics

Add counters and/or histograms at the following points in \`episodic/llm/openai_api/adapter.py\` and \`episodic/llm/openai_api/utils.py\`:

- Preflight budget rejections (\`openai_adapter.preflight_budget_exceeded\`)
- Post-response usage budget rejections (\`openai_adapter.usage_budget_exceeded\`)
- HTTP request latency per attempt in \`_send_once\`
- Retry exhaustion events (\`openai_adapter.retries_exhausted\`)

### 3. Add distributed tracing spans at HTTP boundaries

Instrument \`_send_once\` and \`_send_with_retries\` in \`episodic/llm/openai_api/adapter.py\` with distributed tracing spans (e.g., OpenTelemetry) so operators can observe per-attempt latency and error types in a trace.

## Context

- Observability gap identified during review of PR `#100` (https://github.com/leynos/episodic/pull/100).
- Related issue: `#110` (structured logging at validation/budget failure boundaries).
- Requested by `@leynos`." \
  --assignee leynos

Length of output: 1989



@leynos: Issue #126 has been opened — #126. It covers:

  1. Add chars_per_token to openai_adapter.usage_budget_exceeded log events in _validate_usage_budget.
  2. Add latency and budget-rejection metrics at preflight, usage-budget, retry-exhaustion, and per-attempt HTTP request boundaries.
  3. Add distributed tracing spans in _send_once and _send_with_retries.

@leynos
leynos merged commit 8b0649c into main May 31, 2026
4 checks passed
@leynos
leynos deleted the issue-50-configurable-estimate-token-count-char-ratio branch May 31, 2026 17:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make _estimate_token_count chars-per-token ratio configurable on OpenAICompatibleLLMConfig

1 participant