Skip to content

UN-2946 [FEAT] Prompt Studio lookups bridge, executor hook, and IDE wiring (OSS side)#1929

Open
chandrasekharan-zipstack wants to merge 45 commits intomainfrom
feat/lookups-v2
Open

UN-2946 [FEAT] Prompt Studio lookups bridge, executor hook, and IDE wiring (OSS side)#1929
chandrasekharan-zipstack wants to merge 45 commits intomainfrom
feat/lookups-v2

Conversation

@chandrasekharan-zipstack
Copy link
Copy Markdown
Contributor

What

OSS-side glue for Lookups V2 — a cloud-only Prompt Studio feature that runs an LLM-powered post-extraction enrichment over a prompt's structured output. This PR is the OSS half: a try-import bridge module, executor hook for non-SP runs, IDE wiring for the Raw/Enriched tabs and gating modals, and a few small schema extensions.

The cloud-side PR (where the feature actually lives) is on the unstract-cloud repo, same branch name, same ticket: https://github.com/Zipstack/unstract-cloud/pull/1463.

Why

The cloud plugin needs to integrate with extraction (executor), with the per-prompt output flow (prompt_studio_helper, prompt_studio_output_manager_v2), with export (prompt_studio_registry_helper), and with the IDE UI (Raw/Enriched tabs, gate modal, dirty-seed banner). All of that has to live in OSS — but OSS must never import cloud schema directly. This PR is that boundary.

How

  • One bridge modulebackend/prompt_studio/lookup_utils.py try-imports the cloud plugin and exposes ~10 stable, opaque functions. Every function is a no-op in OSS. Callers don't know cloud key names or model shapes.
  • Executor hooklegacy_executor._run_lookup_enrichment resolves the worker plugin via ExecutorPluginLoader.get("lookup-enrichment") and runs after each prompt's extraction. Skips with a workflow-log entry when the source prompt extracted nothing.
  • Pre-flight blocks — multi-var lookups in non-SP mode return a 400 from fetch_response / bulk_fetch_response, scoped to the prompt set being run.
  • Export gatelookup-validation action on CustomToolView returns the validation buckets that the FE modal renders.
  • Staleness signalCustomTool.last_exported_at (new field) plus is_lookup_dirty in check_deployment_usage powers the "needs re-export" banner.
  • Usage extensionUsage model adds a few cloud-extensible enum choices (reference_type, llm_usage_reason="lookup", status, error_message, execution_time_ms).

Reviewer aid

A condensed one-page primer for human reviewers — system shape, data model, execution diagrams, gates, smoke checklist — is at lookups-v2-reviewer-refresher.html (attached, same file on both PRs). Read it before opening the diff.

The full KB lives in the reviewer's vault at ~/Documents/Obsidian Vault/zipstuff/lookups/.

Main files to review (sorted by significance)

The bridge — central to the OSS↔cloud contract

  1. backend/prompt_studio/lookup_utils.py — every cloud call funnels through here. Tiny, opaque, no-op in OSS.

Executor — non-SP entry
2. workers/executor/executors/legacy_executor.py_run_lookup_enrichment dispatches the worker plugin; new "skip-with-log" branch when extraction yields None.
3. workers/executor/executor_tool_shim.pystream_log routing (confirms the user-facing path for the new skip log).

Prompt Studio integration
4. backend/prompt_studio/prompt_studio_core_v2/views.py — multi-var pre-flight, lookup-validation action, is_lookup_dirty in check_deployment_usage.
5. backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py — three call sites that attach lookup_config to per-prompt output / tool settings.
6. backend/prompt_studio/prompt_studio_registry_v2/prompt_studio_registry_helper.py — export-time validation + per-prompt config attach.
7. backend/prompt_studio/prompt_studio_output_manager_v2/output_manager_helper.py — persists lookup output via the bridge after enrichment.
8. backend/prompt_studio/prompt_studio_core_v2/models.py — adds CustomTool.last_exported_at.

Usage / observability
9. backend/usage_v2/models.py — extensible LLM_USAGE_REASON_CHOICES / REFERENCE_TYPE_CHOICES via try-import; new fields for lookup attribution.

Frontend wiring
10. frontend/src/components/custom-tools/prompt-card/PromptOutput.jsx — Raw vs Enriched tabs.
11. frontend/src/components/custom-tools/header/Header.jsx — wires useLookupExportGate (try-imported, no-op in OSS).
12. frontend/src/components/custom-tools/tool-ide/ToolIde.jsx — wires useLookupDirtySeed.
13. frontend/src/components/custom-tools/combined-output/{CombinedOutput,JsonView}.jsx — combined-output enrichment passthrough.

Can this PR break any existing features?

No production breakage expected. All cloud calls flow through lookup_utils and degrade to None / [] / {ok: True} in OSS, so existing flows behave identically when no lookup is assigned or when the cloud plugin isn't installed.

Watch areas:

  • The new _run_lookup_enrichment runs after every prompt in the non-SP path. When lookup_config is None (no assignment) it returns immediately — should be invisible to non-lookup users.
  • metadata["lookup_outputs"] is a new key on prompt output. FE consumers reflecting the full metadata shape need to ignore unknown keys.
  • Usage table now sees rows with reference_type="lookup_version"; dashboards should treat these as additional rows.
  • The new pre-commit-formatted legacy_executor.py skip log uses shim.stream_log which routes through LogPublisher to the workflow log UI — verified path in executor_tool_shim.py.

Database Migrations

  • usage_v2/0005_usage_reason_ref_created_idx
  • usage_v2/0006_alter_usage_llm_usage_reason_and_more
  • prompt_studio_core_v2/0007_customtool_last_exported_at

Env Config

None new.

Relevant Docs

  • KB: ~/Documents/Obsidian Vault/zipstuff/lookups/.
  • Reviewer refresher (attached): lookups-v2-reviewer-refresher.html.

Related Issues or PRs

Dependencies Versions

No new dependencies.

Notes on Testing

Smoke checklist in the reviewer refresher. The OSS-side hooks and bridge can be exercised against a cloud build; in pure OSS, every lookup-related code path is exercised but no-ops out via LOOKUPS_AVAILABLE = False.

Screenshots

UI screenshots live on the cloud PR (the OSS-side FE changes are wiring only).

Checklist

I have read and understood the Contribution Guidelines.

chandrasekharan-zipstack and others added 29 commits April 22, 2026 19:58
…prompt list endpoint

- Add CustomToolListSerializer for the list action to avoid N+1 queries
  (profile lookups, prompt fetching, coverage calculation per tool)
- Add ToolStudioPromptListSerializer with only prompt_id, prompt_key,
  enforce_type, sequence_number
- Add GET /prompt-studio/prompt/?tool_id={uuid} list endpoint
- List action uses select_related and Subquery annotation for prompt_count
- Detail endpoint unchanged (still uses full CustomToolSerializer)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add lookup-studio plugin detection with dynamic import
- Add PromptStudioPopoverContent for hover submenu (Projects / Look-Ups)
  following the same Popover pattern as HITL and Platform Settings
- Register lookups/* route in useMainAppRoutes.js

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ofile_manager

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…on enrichment

- Add lookup_config export in prompt_studio_helper and registry_helper
  via cloud plugin guard (try/except ImportError)
- Store raw output in PromptStudioOutputManager, enriched in cloud
  LookupOutputResult — preserving both for UI tab display
- Add LookupEnrichmentProtocol and plugin call in post-extraction
  pipeline using ExecutorPluginLoader (no-op in OSS)
- Track lookup LLM usage via standard metrics pipeline
  (usage_kwargs with run_id/execution_id, capture_metrics)
- Move webhook postprocessing from answer_prompt to pipeline
- Frontend: dynamic plugin imports for LookupMenuItem, LookupIndicator,
  LookupOutputTabs in prompt cards; fetch lookup outputs on page load
- Add scroll-to-prompt support via query param in DocumentParser

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…age reason

- Extract get_lookup_config() to prompt_studio/lookup_utils.py, replacing
  4 identical try/except ImportError blocks across prompt_studio_helper
  and prompt_studio_registry_helper
- Add LOOKUP to LLMUsageReason choices (was missing, causing invalid
  choice on usage records from lookup enrichment LLM calls)
- Migration: usage_v2/0004_add_lookup_usage_reason

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Store prompt/completion/total token counts from the most recent
complete() call on the LLM object itself, making usage data
queryable without relying on the Audit pipeline roundtrip.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…hook methods

Move lookup result-application logic to the cloud plugin, matching
the challenge plugin pattern where the plugin owns metadata mutation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…port fallback

Add reusable extraction_complete/extraction_error callback tasks to the
ide_callback worker, replacing the need for Django-based celery workers
for text extraction. Add ExtractionAPIClient for internal API calls.
Add polling fallback to WebSocket transport for local dev reliability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace inline DRAFT lookup check with pluggable cloud-only hook.
Uses try/except ImportError pattern — zero lookup code in OSS.
Collects all DRAFT lookups in one pass with markdown-linked error messages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Consolidate cloud imports into lookup_utils.py with persist_lookup_output() and validate_lookups_for_export() wrappers
- Fix LookupEnrichmentProtocol.run() return type to None matching challenge/evaluation pattern
- Revert logger.info to logger.debug in websocket_views.py
- Eliminate duplicated LookupOutputTabs ternary with renderWithLookupWrapper helper
- Move lookups menu constants from SideNavBar.jsx to cloud plugin
- Harden DocumentParser.jsx scrollTo with UUID validation and fix useEffect dependency
- Revert SocketContext transport to ["websocket"]

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move Prompt Studio / Look-Ups navigation from a hover popover on the
sidebar into a Segmented control within the ToolNavBar. CustomTools
dynamically imports LookupList from the plugin and renders tabs when
available, falling back to projects-only view in OSS mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Switch from eager per-call Audit HTTP push to a deferred batch write
pattern for adapter usage. LLM/embedding calls stash records in-memory;
the executor flushes them into ExecutionResult metadata; the Celery task
batch-writes via a new internal endpoint.

Adds 5 nullable columns to Usage (reference_id, reference_type,
execution_time_ms, status, error_message) and a composite index for
lookup dashboard queries. Extensible choice lists allow cloud plugins
to register additional usage reasons and reference types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…izer

Bridge function in lookup_utils.py lets cloud plugins enrich
PromptStudioOutputSerializer with lookup data (enriched output,
lookup name). Enables real-time lookup results via WebSocket
without page refresh.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… metadata passthrough

- Wire usage_kwargs_extra from lookup config into LLM usage_kwargs for execution observability
- Add error handling around enricher.run() with explicit ERROR usage records
- Generic passthrough of _usage_kwargs into usage records for arbitrary metadata (e.g. reference_id)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…hooks

Add dynamic import of getEnrichedCopyText so the copy button copies
enriched lookup output when the Enriched tab is active. Applied to
both single-pass and multi-profile output paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…modified_at fix

- Add /prompt-studio/<pk>/lookup-validation/ endpoint backing the FE
  Export/Deploy gate; multi-var block check accepts prompt_ids so a single
  prompt run isn't blocked by an unrelated multi-var lookup.
- Add /prompt-output/latest-by-keys/ endpoint that returns the most recent
  raw output per prompt_key for the test panel's "Use Latest Outputs"
  helper.
- Fix prompt output modified_at not refreshing on re-runs (QuerySet.update
  bypasses auto_now); set timezone.now() explicitly in the update args.
- lookup_utils: bridge get_lookup_validation_for_tool and
  get_multi_var_lookups_for_tool with prompt_ids scoping.
- Header wires useLookupExportGate via try-import (no-op stub in OSS).
- TokenUsage treats all-null Usage rows as empty.
- CombinedOutput / JsonView build enriched dict from
  metadata.lookup_outputs to back the Raw|Enriched output toggle.
- .gitignore: widen docker/compose.*.yaml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ab on back

- ProfileInfoBar: swap Row/Col for plain flex-wrap div — kills Ant Row
  negative-margin quirk that overlapped wrapped pills in combined output.
- CustomTools: honor location.state.activeTab so back navigation from
  lookup detail lands on the Look-Ups tab instead of defaulting to Projects.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces nullable last_exported_at on CustomTool (populated on first
successful export) so staleness checks can compare against downstream
mutations without a data backfill. NULL is treated as "unknown" and
suppresses the lookup-dirty flag to avoid false alarms on pre-feature
projects. Adds the get_latest_lookup_mutation_for_tool bridge in
lookup_utils so OSS stays decoupled from the cloud plugin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When enricher.run() raises, surface a user-visible ERROR log line in
the workflow execution log alongside the existing usage record. Keeps
lookup failures observable next to the other pre/post lookup lines we
already emit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Loads useLookupDirtySeed (server-side is_lookup_dirty) and
useLookupExportGate from the cloud plugin via dynamic imports so the
reminder banner reflects lookup changes across page reloads and the
banner's Export flow goes through the same validation modal as the
main buttons. Also adds a titleAdornment slot on ToolNavBar for
rendering the onboarding tooltip and relaxes EmptyState.text to accept
nodes for the tagline + link composition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… state

- Delegate production lookup enrichment to LookupEnrichment.run_with_metrics
  so the executor and the IDE test path share LLM construction, error
  handling, and usage-record emission.
- Let ExecutionLogs callers pass an arbitrary backRouteState via location
  state so nested UI restore (e.g. a sub-tab) no longer needs special
  casing in this component.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bucket of hardening fixes driven by a staff-level PR re-review:

- Org-scope latest_outputs_by_keys (was cross-tenant readable via raw
  .objects.filter() that bypassed OrganizationFilterBackend).
- Hide lookup payload shape from OSS: three new opaque bridge helpers
  (get_original_value_if_enriched, attach_combined_output_enrichment,
  extract_prompt_output_enrichment) replace direct reads of
  metadata["lookup_outputs"] / _lookup_outputs / lookup_outputs in
  output_manager_helper, CombinedOutput.jsx, and usePromptOutput.js.
- Split usage_v2 index into a new 0005 migration that uses
  AddIndexConcurrently + atomic=False so prod doesn't lock the billing
  table during build.
- Delete stale workers/tests/test_usage.py that imported the removed
  UsageHelper module.
- SDK1 LLM gains public get_last_usage_record() so downstream code
  stops reaching into _pending_usage across plugin boundaries.
- legacy_executor stamps metadata["lookup_errors"][prompt] on a failed
  lookup outcome for dashboards that surface partial-failure runs.
- extraction_client docstring notes the cloud-only endpoint contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Module-level probe in prompt_studio/lookup_utils.py — swap per-function
  try/except ImportError for a single LOOKUPS_AVAILABLE flag. Add
  attach_lookup_config / attach_lookup_configs_to_tool_settings helpers
  so the direct metadata["lookup_errors"] write and the
  lookup_config key stamping both route through the bridge.
- Reject org=None in UsageBatchCreateView (usage_v2/internal_views.py).
- Lift useLookupExportGate to a single mount in ToolIde.jsx; thread
  checkLookups down into custom-tools/header/Header.jsx (eliminates
  the double modal-portal risk).
- Delete the direct metadata["lookup_errors"] write from
  workers/executor/executors/legacy_executor.py — flat summary is now
  stamped by LookupEnrichment.write_lookup_error in the cloud plugin.
  Replace hardcoded "lookup_llm" metrics key with lookup_cls.METRICS_KEY.
- Trim boilerplate comments across CombinedOutput, PromptCardItems,
  PromptOutput, usePromptOutput, prompt-card/Header, CustomToolsHelper,
  SideNavBar — keep the why-comments, drop the what-comments.
OSS counterpart to the cloud-side data-model change. Wires the prompt
studio runtime to the new wire shape, surfaces lookup runnability state
in the prompt card, and adds the usage_v2 enum entries the cloud side
records against (lookup as an LLM usage reason, lookup_version as a
reference type).

Partially working — known follow-up:
TODO: rework lookup input UX. The current variable-mapping flow is awkward
(separate rows, manual prompt selection per variable); needs a redesign that
mirrors how users actually compose a lookup template.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bring in:
- modified_at auto-bump fix (BaseModelQuerySet/BaseModelManager + save() override)
- legacy_executor: tool-context warning, backtick log formatting, organization
  IDs threaded into ExecutorToolShim
- prompt-service log streaming + markdown rendering
- minio bucket-listing scope fix and CustomMarkdown URL-safety helper

Conflict resolutions:
- workers/executor/executors/legacy_executor.py: combine HEAD's _usage_records
  field and typed annotations with main's _execution_id/_organization_id
  context fields and tool-context warning. Keep both the usage flush after
  challenge and main's backticked stream_log message.

Drop redundant explicit modified_at workarounds now that BaseModelQuerySet
auto-bumps modified_at on QuerySet.update():
- prompt_studio_output_manager_v2/output_manager_helper.py: remove the
  "modified_at": timezone.now() entry passed to PromptStudioOutputManager
  .objects.filter(...).update(**args), and the now-unused timezone import.
When a configured lookup runs but extraction returned None for the source
prompt, _run_lookup_enrichment used to fall through silently — leaving
users wondering why enrichment didn't appear. Stream a one-line workflow
log via shim.stream_log so the skip is visible alongside other tool-run
events.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds lookup/enrichment plumbing with OSS-safe fallbacks, lookup validation and export staleness tracking, buffered/batched usage reporting with schema migrations and internal bulk API, extraction completion/error callbacks and client, worker-side usage buffering/flush, and multiple frontend integrations for lookup UI and enriched output.

Changes

Cohort / File(s) Summary
Lookup utilities & prompt-studio core
backend/prompt_studio/lookup_utils.py, backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py, backend/prompt_studio/prompt_studio_core_v2/views.py, backend/prompt_studio/prompt_studio_core_v2/urls.py, backend/prompt_studio/prompt_studio_registry_v2/prompt_studio_registry_helper.py
Adds lookup_utils (OSS-safe wrappers vs cloud implementations); injects lookup configs into prompt/tool payloads and exports; adds lookup-validation endpoint; gates execution for multi-variable lookups; records CustomTool.last_exported_at on export and checks lookup staleness.
Prompt-studio output & enrichment
backend/prompt_studio/prompt_studio_output_manager_v2/output_manager_helper.py, .../serializers.py, .../views.py, .../urls.py
Detects/stashes lookup enrichment in prompt outputs, persists lookup payloads (safe DB error handling), avoids per-prompt N+1 when building combined outputs, injects opaque enrichment into combined responses, enriches serializer output with error safety, and adds latest_outputs_by_keys endpoint.
Prompt-studio DB field & migration
backend/prompt_studio/prompt_studio_core_v2/models.py, .../migrations/0007_customtool_last_exported_at.py
Adds nullable CustomTool.last_exported_at DateTimeField and migration.
Prompt list serializers & routing
backend/prompt_studio/prompt_studio_v2/serializers.py, .../views.py, .../urls.py
Adds ToolStudioPromptListSerializer, switches ToolStudioPromptView to use it for list action, and wires a prompt/ list route.
Usage model, migrations & internal bulk API
backend/usage_v2/models.py, backend/usage_v2/migrations/0004_usage_metrics_fields.py, .../0005_usage_reason_ref_created_idx.py, backend/usage_v2/internal_urls.py, backend/usage_v2/internal_views.py
Expands Usage schema (reference fields, execution_time_ms, status, error_message), replaces enum with extensible choices, adds migrations (including concurrent index), and exposes internal /batch/ endpoint for bulk-create usage.
Worker executor & lookup enrichment protocol
workers/executor/executors/legacy_executor.py, workers/executor/executors/answer_prompt.py, workers/executor/executors/plugins/protocols.py
Centralizes per-execution usage collection (_usage_records), buffers/preserves usage on failure, integrates lookup-enrichment and webhook postprocessing into per-prompt flow, and adds LookupEnrichmentProtocol.
Usage helper removal & task flush
workers/executor/executors/usage.py (deleted), workers/executor/tasks.py
Deletes old UsageHelper; Celery task now reads usage_records from execution result metadata and calls UsageAPIClient.bulk_create_usage to persist them (errors logged).
Usage client & worker integration
workers/shared/clients/usage_client.py, workers/shared/clients/extraction_client.py
Fixes BaseUsageResponse.is_success; adds UsageAPIClient.bulk_create_usage; adds ExtractionAPIClient with mark_extraction_complete and mark_extraction_error.
IDE callback extraction tasks
workers/ide_callback/tasks.py
Adds extraction_complete and extraction_error Celery tasks that persist extraction results/errors and optionally emit websocket events.
SDK usage buffering & helpers
unstract/sdk1/src/unstract/sdk1/llm.py, .../usage_handler.py, .../utils/common.py, .../execution/orchestrator.py
Defers immediate pushes by buffering _pending_usage with accessors (get_last_usage, get_last_usage_record, flush_pending_usage); usage/embedding handlers buffer records and flush when called; execution failure metadata now preserves elapsed and usage_records.
Frontend: lookup plugin & enriched UI
frontend/src/components/.../CombinedOutput.jsx, JsonView.jsx, PromptOutput.jsx, PromptCard/*, ToolIde.jsx, CustomTools.jsx, useMainAppRoutes.js, usePromptOutput.js, CustomToolsHelper.js, SideNavBar.jsx, ToolNavBar.jsx, Header.jsx, ListOfTools.jsx, DocumentParser.jsx
Wires optional lookup-studio plugin (dynamic imports with OSS fallbacks): enriched vs raw views, lookup indicators/menus, pre-export validation gates, lookup-dirty seeding, dynamic routes, and multiple UI integrations and hooks.
Frontend: UI tweaks, propTypes & layout
frontend/src/components/.../ProfileInfoBar.jsx, .../ProfileInfoBar.css, TokenUsage.jsx, EmptyState.jsx, RunAllPrompts.jsx, PromptCardItems.jsx, ExecutionLogs.jsx
Layout refactor for ProfileInfoBar, CSS updates, broadened token/null handling, EmptyState.text now PropTypes.node, small UI/prop changes and dynamic imports.
Prompt studio output URL
backend/prompt_studio/prompt_studio_output_manager_v2/urls.py
Adds prompt-output/latest-by-keys/ route.
Tests removed
workers/tests/test_usage.py
Removes tests tied to deleted UsageHelper and its formatting/validation behaviors.
Repo config
.gitignore
Ignores docker/compose.*.yaml (was single-file) and un-ignores docker/compose.debug.yaml.

Sequence Diagram(s)

sequenceDiagram
    participant Executor as Worker Executor
    participant Orchestrator as Orchestrator/Task
    participant WorkerTask as Celery Task
    participant UsageClient as UsageAPIClient
    participant Backend as Backend API
    participant DB as Database

    Executor->>Orchestrator: run execution, collect _usage_records
    Orchestrator->>WorkerTask: return ExecutionResult (metadata includes usage_records)
    WorkerTask->>UsageClient: bulk_create_usage(usage_records, organization_id)
    UsageClient->>Backend: POST /v1/usage/batch/ (records)
    Backend->>DB: INSERT usage rows
    DB-->>Backend: OK
    Backend-->>UsageClient: 200 { created: N } / success
    UsageClient-->>WorkerTask: success/failure
    WorkerTask-->>Orchestrator: log result / continue
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main objective: OSS-side implementation of Lookups V2 bridge, executor hook, and IDE wiring.
Description check ✅ Passed The description is comprehensive and well-structured, covering What, Why, How, risk assessment, migrations, related issues, and testing notes. All major required sections are present and filled out.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/lookups-v2

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.

# Conflicts:
#	backend/prompt_studio/prompt_studio_output_manager_v2/output_manager_helper.py
#	frontend/src/components/custom-tools/prompt-card/PromptCardItems.jsx
Copy link
Copy Markdown
Contributor

@athul-rs athul-rs left a comment

Choose a reason for hiding this comment

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

Code review

Found 4 issues:

  1. APIException(detail=..., code=400) returns HTTP 500, not 400 (DRF's APIException.status_code defaults to 500; the code= kwarg sets the JSON detail.code string, not the HTTP status). Use a ValidationError or a custom exception class with status_code = 400.
  2. _run_webhook_postprocessing is called unconditionally after _apply_type_conversion, so when JSON parsing fails inside handle_json (which sets structured_output[prompt] = {} and returns), the webhook now fires with parsed_data={}. Previously the webhook only ran on successful parses — this is a behavioral regression for any deployment using postprocessing_webhook_url.
  3. LookupEnrichmentProtocol declares only def run(self) -> None, but the call site (legacy_executor.py:2009) invokes lookup_cls.run_with_metrics(...) with 8 keyword args. @runtime_checkable would accept any class with run(), masking a missing run_with_metrics.
  4. Migration 0004 hardcodes ("lookup", "Lookup") into the OSS migration state for Usage.llm_usage_reason, but models.py only adds "lookup" at runtime when the cloud plugin is importable. In OSS-only environments the model and migration choices diverge, so manage.py makemigrations --check will exit non-zero.

Line-anchored notes inline.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

Comment thread backend/prompt_studio/prompt_studio_output_manager_v2/views.py Outdated
Comment thread workers/executor/executors/legacy_executor.py
Comment thread workers/executor/executors/plugins/protocols.py Outdated
Comment thread backend/usage_v2/migrations/0004_usage_metrics_fields.py
When a prompt has a lookup configured, switching its enforce_type to a
complex type (table / line-item / agentic_table) is blocked: the cloud
plugin exposes useEnforceTypeSwitchGate, which OSS dynamically imports
with a no-op fallback. Blocked switches surface an alert and keep the
previous enforce_type.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
backend/prompt_studio/prompt_studio_output_manager_v2/output_manager_helper.py (2)

216-225: ⚠️ Potential issue | 🟠 Major

Narrow lookup persistence error handling; generic catch masks enrichment write failures.

Line 220 catches all exceptions and only logs, so lookup persistence failures are hidden and the request path still looks successful. Catch expected DB/validation failures explicitly and re-raise unknown errors.

#!/bin/bash
# Verify the broad catch and whether any failure signal is propagated.
rg -n -C4 'persist_lookup_output|except Exception|logger.warning' backend/prompt_studio/prompt_studio_output_manager_v2/output_manager_helper.py
rg -n -C3 'lookup_persist_warning|persist_warning|enrichment_warning' backend/prompt_studio
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/prompt_studio/prompt_studio_output_manager_v2/output_manager_helper.py`
around lines 216 - 225, The current broad except Exception around
persist_lookup_output hides unexpected failures; update the block in
output_manager_helper.py that checks prompt_lookup and calls
persist_lookup_output to catch only expected errors (e.g., database/validation
exceptions your codebase uses such as ValueError, ValidationError, or your DB
exception class) and log them with logger.warning including exc_info, but
re-raise any other unknown exceptions so the request path surfaces failures;
specifically modify the try/except that references persist_lookup_output,
prompt_lookup, prompt_output and prompt.prompt_key to only handle known
exception types and use raise for all others after logging.

281-299: ⚠️ Potential issue | 🟠 Major

Default output collector is nondeterministic and does extra DB/plugin work.

Line 287 (exists()) + Line 291 (full iteration) adds extra query/work, and Line 292 overwrites value per row without ordering, so returned output is unstable. Also, the ObjectDoesNotExist branch at Line 298 is not a realistic path for this queryset flow.

Proposed refactor
     def _collect_default_output_for_prompt(
         tool_prompt: ToolStudioPrompt,
         profile_manager_id: str,
         document_manager_id: str,
         enrichment_by_key: dict[str, Any],
     ) -> Any:
         from prompt_studio.lookup_utils import enrich_prompt_output

-        try:
-            queryset = PromptStudioOutputManager.objects.filter(
-                prompt_id=str(tool_prompt.prompt_id),
-                profile_manager=profile_manager_id,
-                is_single_pass_extract=False,
-                document_manager_id=document_manager_id,
-            )
-            if not queryset.exists():
-                return ""
-
-            value: Any = ""
-            for output in queryset:
-                value = output.output
-                enriched = enrich_prompt_output(output, {})
-                bundle = extract_prompt_output_enrichment(enriched)
-                if bundle is not None:
-                    enrichment_by_key[tool_prompt.prompt_key] = bundle
-            return value
-        except ObjectDoesNotExist:
-            return ""
+        output = (
+            PromptStudioOutputManager.objects.filter(
+                prompt_id=str(tool_prompt.prompt_id),
+                profile_manager=profile_manager_id,
+                is_single_pass_extract=False,
+                document_manager_id=document_manager_id,
+            )
+            .order_by("-modified_at")
+            .first()
+        )
+        if output is None:
+            return ""
+
+        enriched = enrich_prompt_output(output, {})
+        bundle = extract_prompt_output_enrichment(enriched)
+        if bundle is not None:
+            enrichment_by_key[tool_prompt.prompt_key] = bundle
+        return output.output
#!/bin/bash
# Verify query shape + unreachable-except pattern in the collector.
rg -n -C5 '_collect_default_output_for_prompt|exists\(|for output in queryset|except ObjectDoesNotExist|order_by' backend/prompt_studio/prompt_studio_output_manager_v2/output_manager_helper.py
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/prompt_studio/prompt_studio_output_manager_v2/output_manager_helper.py`
around lines 281 - 299, The default collector
(_collect_default_output_for_prompt) currently does an exists() then iterates
the entire queryset and overwrites value per row, making results
nondeterministic and doing extra DB/plugin work; change this to fetch a single
deterministic row (e.g., use queryset.order_by("-created_at") or
.order_by("id").first() or .values("output").first()) and only call
enrich_prompt_output/extract_prompt_output_enrichment for that one record to
populate enrichment_by_key[tool_prompt.prompt_key]; remove the try/except
ObjectDoesNotExist branch since .filter().first() will return None rather than
raising and handle the None case by returning "".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/prompt_studio/prompt_studio_core_v2/views.py`:
- Around line 514-517: The lookup gating (_multi_var_lookup_block_response) is
being invoked before validating the required input prompt_id, causing callers
without prompt_id to get a lookup-related 400 instead of a clear validation
error; move the prompt_id presence/validation check so it runs before calling
_multi_var_lookup_block_response (update both occurrences that reference
prompt_id gating), and ensure any early return returns the appropriate
validation error when prompt_id is missing rather than executing the lookup
gate.

In `@frontend/src/components/custom-tools/prompt-card/PromptOutput.jsx`:
- Around line 247-259: The copy action currently calls getEnrichedCopyText
directly so if getEnrichedCopyText throws the enriched text path aborts and the
fallback (displayPromptResult) never runs; create a small safe resolver function
(e.g., resolveSafeCopyText) that takes promptOutputId, promptOutput,
promptDetails and internally calls getEnrichedCopyText inside a try/catch,
returning the enriched text on success or the fallback displayPromptResult(...)
on error/undefined, and then replace both copyToClipboard call sites (the inline
handler using getEnrichedCopyText and the other similar usage) to call
resolveSafeCopyText and pass its result into copyOutputToClipboard to ensure
copy never fails.
- Around line 54-68: The empty catch blocks around the dynamic imports for
LookupOutputTabs and getEnrichedCopyText swallow real errors; update the two
try/catch blocks that import
"../../../plugins/lookup-studio/prompt-card/LookupOutputTabs" and
"../../../plugins/lookup-studio/prompt-card/getEnrichedCopyText" so they
preserve the OSS no-op behavior but log unexpected failures (e.g., catch (err) {
console.error("Failed to load LookupOutputTabs:", err) } or use your app logger)
to surface chunk/load errors while still leaving LookupOutputTabs and
getEnrichedCopyText undefined when the plugin is absent.

---

Duplicate comments:
In
`@backend/prompt_studio/prompt_studio_output_manager_v2/output_manager_helper.py`:
- Around line 216-225: The current broad except Exception around
persist_lookup_output hides unexpected failures; update the block in
output_manager_helper.py that checks prompt_lookup and calls
persist_lookup_output to catch only expected errors (e.g., database/validation
exceptions your codebase uses such as ValueError, ValidationError, or your DB
exception class) and log them with logger.warning including exc_info, but
re-raise any other unknown exceptions so the request path surfaces failures;
specifically modify the try/except that references persist_lookup_output,
prompt_lookup, prompt_output and prompt.prompt_key to only handle known
exception types and use raise for all others after logging.
- Around line 281-299: The default collector
(_collect_default_output_for_prompt) currently does an exists() then iterates
the entire queryset and overwrites value per row, making results
nondeterministic and doing extra DB/plugin work; change this to fetch a single
deterministic row (e.g., use queryset.order_by("-created_at") or
.order_by("id").first() or .values("output").first()) and only call
enrich_prompt_output/extract_prompt_output_enrichment for that one record to
populate enrichment_by_key[tool_prompt.prompt_key]; remove the try/except
ObjectDoesNotExist branch since .filter().first() will return None rather than
raising and handle the None case by returning "".
🪄 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: CHILL

Plan: Pro

Run ID: d7d4bdf8-3be2-42c9-afb3-d9ecfd54448d

📥 Commits

Reviewing files that changed from the base of the PR and between 5e8a223 and f9d3844.

📒 Files selected for processing (11)
  • backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py
  • backend/prompt_studio/prompt_studio_core_v2/views.py
  • backend/prompt_studio/prompt_studio_output_manager_v2/output_manager_helper.py
  • backend/prompt_studio/prompt_studio_registry_v2/prompt_studio_registry_helper.py
  • frontend/src/components/custom-tools/prompt-card/Header.jsx
  • frontend/src/components/custom-tools/prompt-card/PromptCard.jsx
  • frontend/src/components/custom-tools/prompt-card/PromptCardItems.jsx
  • frontend/src/components/custom-tools/prompt-card/PromptOutput.jsx
  • unstract/sdk1/src/unstract/sdk1/llm.py
  • workers/executor/executors/legacy_executor.py
  • workers/ide_callback/tasks.py
✅ Files skipped from review due to trivial changes (1)
  • backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • frontend/src/components/custom-tools/prompt-card/PromptCardItems.jsx
  • backend/prompt_studio/prompt_studio_registry_v2/prompt_studio_registry_helper.py
  • frontend/src/components/custom-tools/prompt-card/Header.jsx

Comment thread backend/prompt_studio/prompt_studio_core_v2/views.py Outdated
Comment thread frontend/src/components/custom-tools/prompt-card/PromptOutput.jsx Outdated
Comment thread frontend/src/components/custom-tools/prompt-card/PromptOutput.jsx
Comment thread workers/executor/executors/plugins/protocols.py Outdated
- Guard embedding flush on callback_manager is None so public-adapter
  embedding usage stops being silently swallowed by the broad except.
- Initialise LegacyExecutor._usage_records in __init__ so an
  early-execute crash can't leave the orchestrator's getattr fallback
  reading None.
- Wrap challenge and summarisation completion calls in try/finally so
  flush_pending_usage() runs even on exception — transient errors
  no longer drop those LLMs' billing rows.
- Replace bare except: pass on the FE error stream with
  logger.debug; the secondary failure is now recoverable.
- Tighten UsageAPIClient.bulk_create_usage success heuristic against
  future partial-body contracts; capture and log the bool return at the
  Celery task with run_id + organization_id.
- Drop redundant organization_id kwarg passed alongside
  set_organization_context in tasks.py.
- SDK records: spread _usage_kwargs first so explicit billing fields
  win; write run_id/llm_usage_reason as None instead of "" so
  UUIDField/choice columns don't reject the row.
- Use rsplit('/', 1)[-1] for display_model so multi-segment IDs
  (e.g. bedrock/anthropic/claude) collapse to the trailing segment.
- Log the litellm cost_per_token failure for embeddings (matches the
  LLM path).
- Tighten LookupEnrichmentProtocol to declare run_with_metrics
  and METRICS_KEY so it actually matches the plugin call site —
  run() -> None was structurally identical to the no-op protocols.
- Wrap the cloud run_with_metrics call in _run_lookup_enrichment
  in a defensive try/except that logs + streams a WARN to the IDE log,
  so plugin contract drift degrades the lookup gracefully instead of
  aborting the answer-prompt batch.
- Hoist llm_cls out of the per-prompt hot loop — it was being
  unpacked from a 7-tuple inside _run_lookup_enrichment on every
  prompt; the caller already has it.
- Extract the is_empty ladder into a module-level _is_blank
  helper so the predicate (and the falsy-but-valid 0/False rationale
  comment) lives next to the predicate, not the executor.
- _run_webhook_postprocessing: when webhook_enabled and output
  is non-JSON, log + shim.stream_log so the user sees the skip in
  the IDE panel instead of silently never receiving the webhook call.
- Narrow persist_lookup_output catch to (IntegrityError,
  ValidationError) and promote to logger.error — broad catches
  were hiding plugin schema drift while reporting success.
- Wrap enrich_prompt_output in the prompt-output serializer's
  to_representation so an enrichment exception no longer 500s the
  list endpoint; matches the surrounding log-and-continue policy.
- UsageStatus(TextChoices) so the status field has an enforced
  domain instead of free-text — producers (llm.py, usage_handler.py)
  already write the canonical "SUCCESS" string so no producer changes
  needed.
- Add usage_reference_pair_consistent CheckConstraint so a row
  with reference_id set but reference_type NULL (or vice versa)
  is rejected at the DB. Cheap to validate on apply because both fields
  landed together in lookups V2 — legacy rows have both NULL. The
  sibling (usage_type, llm_usage_reason) constraint is deferred to
  a follow-up issue: legacy embedding rows have llm_usage_reason=''
  from the old SDK default, and a full-table backfill or default
  ADD CONSTRAINT scan would lock the billing table for too long in
  prod.
- internal_views.py: write llm_usage_reason as None instead of
  "" when missing so the choice column doesn't store an
  out-of-domain value.
- lookup_utils.py: narrow the ImportError catch to the four cloud
  lookup modules so a failing transitive import inside the cloud plugin
  re-raises instead of silently degrading the whole feature to a no-op.
  Annotate get_original_value_if_enriched return as
  tuple[Any, dict] | None and rephrase the docstring to match the
  actual shape callers tuple-unpack.
- Drop the attach_lookup_config /
  attach_lookup_configs_to_tool_settings /
  get_lookup_config_from_output wrappers — they were trivial dict
  ops over a hardcoded key already used directly by the executor and
  single-pass plugin, so the "key owned by the bridge" framing was
  misleading. Inline at all five callsites.
- Drop the "future prompt_studio" forward-looking comment in
  ide_callback/tasks.py — only source='lookup' is wired up, and
  the cloud lookups plugin is the only registrant of the underlying
  endpoints.
- latest_outputs_by_keys: switch the per-prompt latest pick to
  order_by('prompt_id', '-modified_at').distinct('prompt_id') so
  Postgres returns at most one row per prompt instead of materialising
  every historical run + relying on a Python break.
- fetch_default_output_response: collapse the previous N+1
  (exists() + for output in queryset per prompt) into a single
  DISTINCT ON (prompt_id, profile_manager_id) query, plus a
  per-tool cache for the default-profile lookup. Combined Output is a
  hot path — the old shape made every panel switch O(prompts × runs)
  with a plugin invocation per matching row.
- Drop the unused _resolve_profile_for_prompt and
  _collect_default_output_for_prompt helpers, and the dead
  except ObjectDoesNotExist: return '' (neither .exists() nor
  the queryset iteration ever raised that exception).
- PromptOutput.jsx: replace empty catch {} on the lookup
  dynamic-import sites with console.warn so unexpected chunk-load
  failures don't masquerade as OSS-mode behaviour. Add a
  resolveCopyText helper that wraps getEnrichedCopyText in
  try/catch + fallback so a plugin-side throw can't break the Copy
  button at either of the two call sites.
- usePromptOutput.js: same catch (error) -> console.warn for
  the two existing dynamic-import sites; wrap the per-item
  handleLookupOutput call in try/catch so a single bad enrichment
  payload no longer aborts the surrounding forEach and skips the
  prompt-output state update.
- prompt_studio_core_v2/views.py: validate prompt_id before
  the multi-var lookup gate so a missing field returns a clean 400
  instead of a lookup-related error.
- ide_callback/tasks.py: (result_dict.get('data') or {}) so an
  explicit data=None from the executor doesn't AttributeError into
  a generic ERROR callback. Replace the inner except: pass swallow
  with logger.debug so a secondary WS-emit failure during the
  outer extraction_complete fallback is recoverable from logs.
….debug

- _run_webhook_postprocessing: skip when structured_output[prompt_name]
  is empty or non-iterable. Pre-refactor, the webhook lived inside
  handle_json after its parse-failure early-return (which sets the
  output to {}), so a malformed JSON answer never dispatched the
  webhook — the new explicit gate restores that behaviour. Subscribers
  no longer receive empty-payload calls they didn't see before.
- .gitignore: re-include docker/compose.debug.yaml after the
  broader docker/compose.*.yaml rule so a delete + recreate doesn't
  make the tracked file look untracked, and so teammate-added compose
  files aren't silently masked.
`APIException(code=400)` only sets `detail.code` in the JSON body;
`status_code` is hardcoded to HTTP_500_INTERNAL_SERVER_ERROR. Switch to
`ValidationError` so the missing-tool_id and tool-not-found branches in
`latest_outputs_by_keys` and `get_output_for_tool_default` actually
respond with 400 as the comment / docstring imply.
@chandrasekharan-zipstack
Copy link
Copy Markdown
Contributor Author

Review comment dispositions

Thanks for the thorough reviews. The latest round of commits on this branch (d8986974d2fd9cf5d0) addresses most of the comments in-place. Below maps each thread to its disposition; commit SHAs link to the actual change.

Fixed in this round

Reviewer Path:Line Resolution
@jaseemjaskp legacy_executor.py:1801 (callback_manager None) d8986974d — explicit callback_manager is None guard; broad except removed
@jaseemjaskp / @greptile-apps / @coderabbitai tasks.py:109 / usage_client.py:174,199 (bulk_create_usage drops) d8986974d — capture return value, log at ERROR with run_id+org+count; tighten success heuristic
@jaseemjaskp legacy_executor.py:88 (_usage_records init) d8986974d — initialised in __init__ so orchestrator getattr always sees a real list
@jaseemjaskp legacy_executor.py:154 (bare except: pass) d8986974d — replaced with logger.debug(..., exc_info=True)
@jaseemjaskp / @greptile-apps legacy_executor.py:2034 (webhook silently disabled for non-JSON) 9ef8c800f — emits logger.warning + shim.stream_log(level=WARN)
@athul-rs legacy_executor.py:1700 (webhook fires after JSON parse failure) 301dc9bfc — gate _run_webhook_postprocessing on parsed payload being a non-empty dict/list
@jaseemjaskp usage_handler.py:118 (embedding cost silently $0.00) d8986974d — mirror the LLM path's logger.warning(..., exc_info=True)
@coderabbitai usage_handler.py:122 (embed_model is None guard) d8986974d — early-return guard
@coderabbitai usage_handler.py:138 (run_id="" → UUIDField) d8986974d — write None instead of "" for run_id and llm_usage_reason
@jaseemjaskp / @coderabbitai llm.py:716,737 (split + _usage_kwargs placement) d8986974drsplit("/", 1)[-1] for multi-segment; _usage_kwargs spread first so explicit fields win
@coderabbitai legacy_executor.py:2274 (summarisation flush gap) d8986974d — wrapped completion+flush in try/finally; same pattern applied to challenge LLM
@coderabbitai tasks.py:119 (redundant org param) d8986974d — drop the kwarg, rely on set_organization_context
@jaseemjaskp / @greptile-apps / @athul-rs / @coderabbitai protocols.py:55 (LookupEnrichmentProtocol wrong contract) 9ef8c800f — declares run_with_metrics + METRICS_KEY; matches the executor call site
@coderabbitai legacy_executor.py:2020 (defensive plugin call) 9ef8c800f — wraps lookup_cls.run_with_metrics in try/except + WARN stream
@greptile-apps legacy_executor.py:2011 (_get_prompt_deps() per-prompt) 9ef8c800fllm_cls plumbed in by the caller; lookup helper no longer re-unpacks the 7-tuple
@jaseemjaskp legacy_executor.py:1989 (is_empty ladder) 9ef8c800f — extracted module-level _is_blank helper
@coderabbitai serializers.py:52 (enrich_prompt_output fail-open) 9ef8c800f — wrapped in try/except matching surrounding token-usage / coverage policy
@jaseemjaskp output_manager_helper.py:219 (persist_lookup_output broad except) 9ef8c800f — narrowed to (IntegrityError, ValidationError), promoted to logger.error
@jaseemjaskp models.py:117 (status no choices) 29a705047UsageStatus(TextChoices)
@jaseemjaskp models.py:91 ((reference_id, reference_type) invariant) 29a705047 — added usage_reference_pair_consistent CheckConstraint
@jaseemjaskp lookup_utils.py:107 (return-shape mismatch) 29a705047-> tuple[Any, dict] | None; docstring rewritten
@jaseemjaskp lookup_utils.py:1,166 (docstring overstatement) 29a705047 — narrowed except ImportError to the four cloud lookup modules; rephrased docstring
@jaseemjaskp lookup_utils.py:124 (trivial wrappers) 29a705047 — dropped wrappers, inlined the literal at all 5 OSS callsites
@jaseemjaskp llm.py:645 (get_last_usage docstring) d8986974d — clarified to "sync, async, or streaming"
@jaseemjaskp tasks.py:524 ("future prompt_studio") 29a705047 — drops the forward-looking comment
@jaseemjaskp / @coderabbitai views.py:100 (latest_outputs_by_keys no LIMIT) 993ae95d1DISTINCT ON("prompt_id")
@jaseemjaskp output_manager_helper.py:293 (fetch_default_output_response N+1) 993ae95d1 — single DISTINCT ON (prompt_id, profile_manager_id) query + per-tool default-profile cache
@jaseemjaskp output_manager_helper.py:260 (dead except ObjectDoesNotExist) 993ae95d1_collect_default_output_for_prompt removed entirely as part of the N+1 refactor
@coderabbitai usePromptOutput.js:148 (per-item failure aborts loop) cc544e476 — try/catch around handleLookupOutput + console.warn
@coderabbitai PromptOutput.jsx:60,68 (empty catch {}) cc544e476console.warn(error)
@coderabbitai PromptOutput.jsx:248,504 (getEnrichedCopyText can throw) cc544e476resolveCopyText(promptOutputId, fallback) helper used at both copy sites
@coderabbitai views.py:514 (lookup gate before prompt_id validation) cc544e476 — prompt_id validation moved up
@coderabbitai tasks.py:586 (result_dict.get("data", {}) None) cc544e476(result_dict.get("data") or {})
@coderabbitai tasks.py:634 (inner except: pass) cc544e476logger.debug(..., exc_info=True)
@jaseemjaskp .gitignore:656 (broader pattern masks compose.debug) 301dc9bfc — added !docker/compose.debug.yaml exception
@athul-rs views.py:80 (returns 500 not 400) 2fd9cf5d0 — switched to ValidationError (returns 400) at all three sites

Already addressed in earlier rounds (this branch)

Reviewer Path:Line Earlier commit
@greptile-apps legacy_executor.py:1972 (current_value is None only) a4679b11d_is_blank ladder (now extracted to a helper in 9ef8c800f)
@greptile-apps internal_views.py:160 (unbounded bulk_create) a4679b11dbatch_size=500
@greptile-apps / @athul-rs migrations/0004 (cloud-only choices drift) 5e8a22329 — static-union choices on the OSS side

Stale

Reviewer Comment Note
@jaseemjaskp models.py:74 (LLM_USAGE_REASON_CHOICES regresses TextChoices invariants) Partially superseded by 5e8a22329. The runtime-mutation and silent-except Exception concerns are gone (the choices list is now a static union, no try-import). The remaining concern — no LLMUsageReason.LOOKUP constant for type-safe references — is a knowingly-accepted trade-off so the OSS migration state matches the cloud migration state without per-build divergence. Happy to revisit if a typed constant module pattern emerges.
@jaseemjaskp migrations/0005_usage_reason_ref_created_idx.py (concurrent-index smoke test) The migrations were squashed earlier on this branch (a23c7a61b); migration 0005 is the concurrent-index split, but the squash means there are also no separate 0001..0009 migrations to test. A targeted smoke test for 0005's atomic=False + index name is reasonable as a follow-up; not blocking this PR.

Deferred to follow-ups

Reviewer Comment Why deferred
@jaseemjaskp models.py:74 ((usage_type, llm_usage_reason) cross-field CheckConstraint) The constraint is correct, but applying it on a large prod usage table is expensive: legacy embedding rows have llm_usage_reason='' from the old SDK default, and Django's default ADD CONSTRAINT validates the entire table under an ACCESS EXCLUSIVE-style lock. A safe rollout needs ADD CONSTRAINT … NOT VALID + batched VALIDATE CONSTRAINT during a maintenance window — to be tracked as a follow-up. New-row producers (SDK + view) already coerce missing values to None, so future writes are constraint-compatible.
@jaseemjaskp usage_client.py:172 (typed UsageRecord dataclass) Multi-day refactor across sdk1 + backend + workers. Will file as a follow-up.
@jaseemjaskp tasks.py:535 / internal_urls.py:21 / llm.py:669 / usage_client.py:174 / lookup_utils.py:13 (5 test-coverage items) Out of scope for this round per branch owner. Will pick up in a follow-up test-coverage pass.
@jaseemjaskp tasks.py:550 (extraction_complete/error scaffolding duplication) Pairs with the test-coverage follow-up above.

Skipped — out of scope

Reviewer Comment Note
@coderabbitai views.py:927 (ProfileManager queryset/permission concern) Pre-existing concern unrelated to lookups V2; will route to a separate ticket.
@coderabbitai utils/common.py:258 (time_taken(s) unit) Pre-existing; not introduced in this PR.

🤖 Generated with Claude Code

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

@chandrasekharan-zipstack — thank you for the detailed and well-structured disposition table. This is exactly the kind of thorough accounting that makes large PRs reviewable. Here's my read on all the items attributed to me:

✅ All @coderabbitai-flagged issues are resolved or acceptably dispositioned:

Item Verdict
usage_handler.pyembed_model is None guard + run_id=""→None ✅ Fixed (d8986974d)
llm.pyrsplit + _usage_kwargs field-win ordering ✅ Fixed (d8986974d)
legacy_executor.py — summarisation flush in try/finally ✅ Fixed (d8986974d)
tasks.py — redundant org param drop ✅ Fixed (d8986974d)
protocols.pyrun_with_metrics + METRICS_KEY contract ✅ Fixed (9ef8c800f)
legacy_executor.py — defensive plugin call wrap ✅ Fixed (9ef8c800f)
serializers.pyenrich_prompt_output fail-open → try/except ✅ Fixed (9ef8c800f)
views.pylatest_outputs_by_keys unbounded → DISTINCT ON ✅ Fixed (993ae95d1)
usePromptOutput.js / PromptOutput.jsx — error isolation + resolveCopyText ✅ Fixed (cc544e476)
views.py:514 — lookup gate ordering ✅ Fixed (cc544e476)
tasks.py(... or {}) None-safety + inner except: pass ✅ Fixed (cc544e476)
views.py:927 — ProfileManager concern ✅ Accepted as pre-existing / separate ticket
utils/common.py:258time_taken unit ✅ Accepted as pre-existing

The engineering rationale for the deferred items is sound — especially the ADD CONSTRAINT … NOT VALID + batched VALIDATE CONSTRAINT approach for the cross-field check on a large production table, and deferring the UsageRecord typed dataclass to avoid a cross-cutting multi-package refactor in the same PR.

I'll resolve my open threads and run a fresh review pass against the latest commits.

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (5)
workers/ide_callback/tasks.py (1)

592-598: ⚠️ Potential issue | 🟠 Major

Treat missing extraction callback endpoints as terminal OSS no-ops.

These endpoints are documented as cloud-only in workers/shared/clients/extraction_client.py, but both calls currently treat a 404 like a real failure. In OSS builds that means extraction_complete() can re-raise into retries, and extraction_error() just logs noise instead of exiting cleanly. Catch the client error type here and swallow HTTP 404 explicitly.

Also applies to: 666-671

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@workers/ide_callback/tasks.py` around lines 592 - 598, The calls to
api.mark_extraction_complete and the corresponding extraction_error handler
should treat a 404 from the extraction client as a terminal no-op for OSS;
update extraction_complete() and extraction_error() to catch the extraction
client exception (the client error type thrown by the extraction client) around
the api.mark_extraction_complete / api.mark_extraction_error calls, check if the
exception represents HTTP 404 and if so swallow it and return/exit cleanly,
otherwise re-raise or propagate the error as before so real failures still
surface.
workers/executor/executors/legacy_executor.py (1)

2038-2067: ⚠️ Potential issue | 🟠 Major

Keep lookup enrichment resilient after the plugin call returns.

The new try/except only protects run_with_metrics() itself. If the plugin returns an object missing usage_records, llm_metrics, or METRICS_KEY, lines 2066-2067 still raise and fail the prompt after extraction already succeeded. Guard those attributes and skip enrichment when the outcome shape is invalid.

Suggested fix
-        self._usage_records.extend(outcome.usage_records)
-        metrics.setdefault(prompt_name, {})[lookup_cls.METRICS_KEY] = outcome.llm_metrics
+        usage_records = getattr(outcome, "usage_records", None)
+        llm_metrics = getattr(outcome, "llm_metrics", None)
+        metrics_key = getattr(lookup_cls, "METRICS_KEY", None)
+
+        if isinstance(usage_records, list):
+            self._usage_records.extend(usage_records)
+        if metrics_key is not None and llm_metrics is not None:
+            metrics.setdefault(prompt_name, {})[metrics_key] = llm_metrics
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@workers/executor/executors/legacy_executor.py` around lines 2038 - 2067, The
current try/except only wraps lookup_cls.run_with_metrics and then assumes
outcome has the expected shape; validate the returned outcome before mutating
state: after calling lookup_cls.run_with_metrics (or inside the same try block),
check that outcome has an iterable usage_records and a llm_metrics attribute and
that lookup_cls.METRICS_KEY is defined; if any of these are missing or invalid,
log the error with logger.exception, call shim.stream_log with a WARN message
similar to the existing one, and return/skip enrichment instead of calling
self._usage_records.extend or writing into metrics; keep these checks adjacent
to lookup_cls.run_with_metrics so malformed plugin returns are handled the same
way as raised exceptions.
workers/executor/tasks.py (1)

112-123: ⚠️ Potential issue | 🟠 Major

Escalate a failed usage flush instead of only logging it.

bulk_create_usage() already has a non-exception failure signal. This branch logs and then returns the original extraction result, so a transient usage API outage still drops the whole batch permanently and Celery never retries. Re-raise here, or push the records to a retryable fallback/DLQ when ok is false.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@workers/executor/tasks.py` around lines 112 - 123, The failure branch after
calling usage_client.bulk_create_usage(usage_records) only logs and returns,
which silently drops records; change it so a non-exception failure becomes
retryable: when ok is False (inside the try surrounding
usage_client.bulk_create_usage in tasks.py) either raise a specific exception
(e.g., UsageFlushError with run_id/organization_id/record_count in the message)
to let Celery retry, or push the usage_records into your retryable fallback/DLQ
before returning; update the logger.error to include the same contextual details
and then raise or enqueue so transient outages don't permanently drop the batch.
backend/prompt_studio/prompt_studio_output_manager_v2/views.py (1)

74-95: ⚠️ Potential issue | 🟡 Minor

Reject malformed tool_id before the ORM filter.

This only handles the missing case. A non-UUID string still flows into ToolStudioPrompt.objects.filter(tool_id=tool_id, ...); with tool_id backed by a UUID FK, Django raises before the query executes, so this action still returns a 500 instead of the intended 400. The same guard should be reused in get_output_for_tool_default() below.

🔧 Suggested guard
+        from uuid import UUID
+
         tool_id = request.GET.get("tool_id")
         keys_param = request.GET.get("prompt_keys", "")
         if not tool_id:
             # ``APIException(code=400)`` only sets ``detail.code`` in the body;
             # ``status_code`` is hardcoded to 500. Use ``ValidationError`` so
             # callers get the intended 400.
             raise ValidationError(detail=PromptOutputManagerErrorMessage.TOOL_VALIDATION)
+        try:
+            UUID(str(tool_id))
+        except (TypeError, ValueError):
+            raise ValidationError(
+                detail=PromptOutputManagerErrorMessage.TOOL_VALIDATION
+            )
#!/bin/bash
# Verify that ToolStudioPrompt.tool_id points to a UUID-backed CustomTool key.
set -e
fd -i '^models\.py$' backend/prompt_studio -x rg -n -C2 'class ToolStudioPrompt|class CustomTool|tool_id\s*=|prompt_id\s*=' {}

Expected result: ToolStudioPrompt.tool_id resolves to CustomTool, and CustomTool uses a UUID-backed identifier. If so, malformed UUIDs should be rejected before the ORM filter.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/prompt_studio/prompt_studio_output_manager_v2/views.py` around lines
74 - 95, Reject malformed tool_id before it reaches the ORM by validating its
UUID form right after obtaining tool_id from request.GET; replace the current
"if not tool_id" check with a guard that also attempts to parse tool_id as a
UUID (or use Django's UUIDField validator) and raises
ValidationError(detail=PromptOutputManagerErrorMessage.TOOL_VALIDATION) on parse
failure, and apply the same validation in get_output_for_tool_default() so
ToolStudioPrompt.objects.filter(tool_id=tool_id, ...) never receives an invalid
UUID string.
backend/prompt_studio/prompt_studio_output_manager_v2/output_manager_helper.py (1)

350-355: ⚠️ Potential issue | 🟠 Major

Keep combined-output enrichment fail-open as well.

result already has the raw values here, but enrich_prompt_output() and attach_combined_output_enrichment() still cross the cloud lookup bridge without a guard. One plugin-side exception will 500 the whole combined-output endpoint again instead of returning the raw payload.

🛡️ Suggested guard
-            enriched = enrich_prompt_output(output, {})
-            bundle = extract_prompt_output_enrichment(enriched)
-            if bundle is not None:
-                enrichment_by_key[tool_prompt.prompt_key] = bundle
+            try:
+                enriched = enrich_prompt_output(output, {})
+                bundle = extract_prompt_output_enrichment(enriched)
+                if bundle is not None:
+                    enrichment_by_key[tool_prompt.prompt_key] = bundle
+            except Exception:
+                logger.exception(
+                    "Failed to enrich combined output for prompt %s",
+                    tool_prompt.prompt_key,
+                )
 
-        attach_combined_output_enrichment(result, enrichment_by_key)
+        try:
+            attach_combined_output_enrichment(result, enrichment_by_key)
+        except Exception:
+            logger.exception("Failed to attach combined output enrichment")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/prompt_studio/prompt_studio_output_manager_v2/output_manager_helper.py`
around lines 350 - 355, Wrap the enrichment/attachment steps in a try/except so
plugin/cloud lookup failures don't crash the combined-output endpoint: when
iterating over tool prompts, call enrich_prompt_output(output, {}) and
extract_prompt_output_enrichment(enriched) inside a try block and only add to
enrichment_by_key on success (use tool_prompt.prompt_key), and then call
attach_combined_output_enrichment(result, enrichment_by_key) inside its own
try/except (or skip if enrichment_by_key is empty); on exception log the error
and proceed to return the existing raw result unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py`:
- Around line 1180-1182: The legacy single-pass flow is missing lookup_configs:
when building the legacy single-pass payload in _fetch_single_pass_response
(which constructs single_pass_extraction) you must include the same
lookup_configs produced by get_lookup_configs_for_tool(tool, prompts=prompts) so
lookups continue to work; either call get_lookup_configs_for_tool there and add
the result into the single_pass_extraction/tool_settings, or refactor to reuse
build_single_pass_payload to produce a canonical payload that both
prompt_responder() and _fetch_single_pass_response() consume, ensuring
lookup_configs is present in both paths.

In `@backend/prompt_studio/prompt_studio_core_v2/views.py`:
- Around line 109-123: The guard currently checks the tool's setting via
custom_tool.single_pass_extraction_mode which allows callers like
fetch_response() and bulk_fetch_response() to bypass the multi-variable lookup
protection; change the helper to accept an explicit is_single_pass boolean
(e.g., add parameter is_single_pass=False) and use that to decide whether to
skip the block instead of reading custom_tool.single_pass_extraction_mode,
update all callers: call with is_single_pass=False from fetch_response() and
bulk_fetch_response(), and either call with is_single_pass=True (or skip the
helper) from single_pass_extraction(); keep the existing Response behavior and
use get_multi_var_lookups_for_tool and names logic unchanged.

In
`@backend/prompt_studio/prompt_studio_registry_v2/prompt_studio_registry_helper.py`:
- Around line 301-304: The lookup validation is being run against the full
prompts list (lookup_configs, lookup_error =
validate_lookups_for_export(prompts)) before the export loop filters out NOTES
and inactive prompts, causing spurious failures; update the code to perform
validate_lookups_for_export on the filtered set that will actually be exported
(i.e., the prompts after the logic that drops NOTES and inactive entries) or
pass the already-filtered collection into validate_lookups_for_export so only
exportable prompts are validated (refer to validate_lookups_for_export and the
export filtering loop that removes NOTES/inactive prompts).

In `@unstract/sdk1/src/unstract/sdk1/usage_handler.py`:
- Around line 108-154: The code assumes self.token_counter exists when recording
embeddings; guard accesses to avoid AttributeError by checking if
self.token_counter is not None before reading total_embedding_token_count and
before calling reset_counts in the embedding branch (where embed_model is
checked). If token_counter is None, use 0 for embedding_tokens (and any token
counts) and skip calling reset_counts; keep the rest of the _pending_usage
payload the same. Reference symbols: self.token_counter,
total_embedding_token_count, reset_counts, embed_model, and the block that
appends to self._pending_usage.

---

Duplicate comments:
In
`@backend/prompt_studio/prompt_studio_output_manager_v2/output_manager_helper.py`:
- Around line 350-355: Wrap the enrichment/attachment steps in a try/except so
plugin/cloud lookup failures don't crash the combined-output endpoint: when
iterating over tool prompts, call enrich_prompt_output(output, {}) and
extract_prompt_output_enrichment(enriched) inside a try block and only add to
enrichment_by_key on success (use tool_prompt.prompt_key), and then call
attach_combined_output_enrichment(result, enrichment_by_key) inside its own
try/except (or skip if enrichment_by_key is empty); on exception log the error
and proceed to return the existing raw result unchanged.

In `@backend/prompt_studio/prompt_studio_output_manager_v2/views.py`:
- Around line 74-95: Reject malformed tool_id before it reaches the ORM by
validating its UUID form right after obtaining tool_id from request.GET; replace
the current "if not tool_id" check with a guard that also attempts to parse
tool_id as a UUID (or use Django's UUIDField validator) and raises
ValidationError(detail=PromptOutputManagerErrorMessage.TOOL_VALIDATION) on parse
failure, and apply the same validation in get_output_for_tool_default() so
ToolStudioPrompt.objects.filter(tool_id=tool_id, ...) never receives an invalid
UUID string.

In `@workers/executor/executors/legacy_executor.py`:
- Around line 2038-2067: The current try/except only wraps
lookup_cls.run_with_metrics and then assumes outcome has the expected shape;
validate the returned outcome before mutating state: after calling
lookup_cls.run_with_metrics (or inside the same try block), check that outcome
has an iterable usage_records and a llm_metrics attribute and that
lookup_cls.METRICS_KEY is defined; if any of these are missing or invalid, log
the error with logger.exception, call shim.stream_log with a WARN message
similar to the existing one, and return/skip enrichment instead of calling
self._usage_records.extend or writing into metrics; keep these checks adjacent
to lookup_cls.run_with_metrics so malformed plugin returns are handled the same
way as raised exceptions.

In `@workers/executor/tasks.py`:
- Around line 112-123: The failure branch after calling
usage_client.bulk_create_usage(usage_records) only logs and returns, which
silently drops records; change it so a non-exception failure becomes retryable:
when ok is False (inside the try surrounding usage_client.bulk_create_usage in
tasks.py) either raise a specific exception (e.g., UsageFlushError with
run_id/organization_id/record_count in the message) to let Celery retry, or push
the usage_records into your retryable fallback/DLQ before returning; update the
logger.error to include the same contextual details and then raise or enqueue so
transient outages don't permanently drop the batch.

In `@workers/ide_callback/tasks.py`:
- Around line 592-598: The calls to api.mark_extraction_complete and the
corresponding extraction_error handler should treat a 404 from the extraction
client as a terminal no-op for OSS; update extraction_complete() and
extraction_error() to catch the extraction client exception (the client error
type thrown by the extraction client) around the api.mark_extraction_complete /
api.mark_extraction_error calls, check if the exception represents HTTP 404 and
if so swallow it and return/exit cleanly, otherwise re-raise or propagate the
error as before so real failures still surface.
🪄 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: CHILL

Plan: Pro

Run ID: 0e13657d-89a4-4202-ab9f-951b5ca9868a

📥 Commits

Reviewing files that changed from the base of the PR and between f9d3844 and 2fd9cf5.

📒 Files selected for processing (20)
  • .gitignore
  • backend/prompt_studio/lookup_utils.py
  • backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py
  • backend/prompt_studio/prompt_studio_core_v2/views.py
  • backend/prompt_studio/prompt_studio_output_manager_v2/output_manager_helper.py
  • backend/prompt_studio/prompt_studio_output_manager_v2/serializers.py
  • backend/prompt_studio/prompt_studio_output_manager_v2/views.py
  • backend/prompt_studio/prompt_studio_registry_v2/prompt_studio_registry_helper.py
  • backend/usage_v2/internal_views.py
  • backend/usage_v2/migrations/0006_alter_usage_status_and_more.py
  • backend/usage_v2/models.py
  • frontend/src/components/custom-tools/prompt-card/PromptOutput.jsx
  • frontend/src/hooks/usePromptOutput.js
  • unstract/sdk1/src/unstract/sdk1/llm.py
  • unstract/sdk1/src/unstract/sdk1/usage_handler.py
  • workers/executor/executors/legacy_executor.py
  • workers/executor/executors/plugins/protocols.py
  • workers/executor/tasks.py
  • workers/ide_callback/tasks.py
  • workers/shared/clients/usage_client.py
✅ Files skipped from review due to trivial changes (1)
  • workers/executor/executors/plugins/protocols.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • .gitignore
  • frontend/src/hooks/usePromptOutput.js
  • frontend/src/components/custom-tools/prompt-card/PromptOutput.jsx
  • backend/usage_v2/internal_views.py
  • backend/prompt_studio/lookup_utils.py

Comment thread backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py
Comment thread backend/prompt_studio/prompt_studio_core_v2/views.py Outdated
Comment thread backend/prompt_studio/prompt_studio_registry_v2/prompt_studio_registry_helper.py Outdated
Comment thread unstract/sdk1/src/unstract/sdk1/usage_handler.py
Comment thread workers/executor/executors/legacy_executor.py
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (10)
workers/executor/tasks.py (1)

113-130: ⚠️ Potential issue | 🟠 Major

Usage flush failure is still non-recoverable (records can be dropped).

When bulk_create_usage fails, this path only logs and continues. That still allows silent billing-row loss on transient outages.

Proposed fix
             if not ok:
                 logger.error(
                     "bulk_create_usage returned failure for %d records "
                     "(run_id=%s organization_id=%s)",
                     len(usage_records),
                     context.run_id,
                     context.organization_id,
                 )
+                raise ConnectionError("bulk_create_usage returned failure")
         except Exception:
             logger.error(
                 "Failed to flush %d usage records for run_id=%s organization_id=%s",
                 len(usage_records),
                 context.run_id,
                 context.organization_id,
                 exc_info=True,
             )
+            raise
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@workers/executor/tasks.py` around lines 113 - 130, The current flush path in
tasks.py logs failures from bulk_create_usage but swallows them, risking dropped
billing rows; modify the failure handling inside the block that calls
bulk_create_usage so that on a non-ok result or on exception you propagate an
error to the caller (either by re-raising the caught exception or raising a
RecoverableError) instead of only logging, so the orchestration can retry; keep
the existing logger.error messages (including run_id and organization_id) but
follow them with a raise to fail the task and preserve usage_records/context for
retry handling.
backend/usage_v2/internal_views.py (1)

164-193: ⚠️ Potential issue | 🟠 Major

Validate record shape before building Usage objects.

Current permissive defaults (usage_type, adapter_instance_id, model_name, etc.) can mask producer bugs, and one malformed row can fail the entire batch write.

Proposed fix sketch
         usage_objects = []
-        for r in records:
+        for i, r in enumerate(records):
+            if not isinstance(r, dict):
+                return JsonResponse(
+                    {"success": False, "error": f"Invalid record at index {i}"},
+                    status=status.HTTP_400_BAD_REQUEST,
+                )
+            missing = [k for k in ("usage_type", "adapter_instance_id", "model_name") if k not in r]
+            if missing:
+                return JsonResponse(
+                    {
+                        "success": False,
+                        "error": f"Missing required keys at index {i}: {', '.join(missing)}",
+                    },
+                    status=status.HTTP_400_BAD_REQUEST,
+                )
             usage_objects.append(
                 Usage(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/usage_v2/internal_views.py` around lines 164 - 193, The loop that
constructs Usage objects from records (the for r in records -> Usage(...) block)
is too permissive and can mask producer bugs; add explicit validation before
building each Usage: verify required fields (e.g., workflow_id, execution_id,
usage_type, adapter_instance_id or run_id as applicable) are present and of the
expected type, coerce/validate numeric fields (embedding_tokens, prompt_tokens,
completion_tokens, total_tokens as ints; cost_in_dollars as float), ensure
model_name is non-empty when usage_type == "llm", and enforce the
llm_usage_reason None/coercion rule; for records that fail validation, log a
clear error including the offending record and either skip the record or
collect/report them so the batch write isn't aborted by a single malformed row,
then only append validated Usage instances to usage_objects.
unstract/sdk1/src/unstract/sdk1/usage_handler.py (1)

108-112: ⚠️ Potential issue | 🟠 Major

Guard token_counter before dereference in embedding callback.

token_counter is nullable, but Line 111 and Line 154 assume it is always present. This can raise AttributeError in production callback flows and skip usage persistence.

Proposed fix
         ):
             if self.embed_model is None:
                 return
+            if self.token_counter is None:
+                logger.warning(
+                    "Embedding usage callback invoked without token_counter; skipping usage record"
+                )
+                return
             model_name = self.embed_model.model_name
             embedding_tokens = self.token_counter.total_embedding_token_count
@@
-            self.token_counter.reset_counts()
+            self.token_counter.reset_counts()

Also applies to: 154-154

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@unstract/sdk1/src/unstract/sdk1/usage_handler.py` around lines 108 - 112, The
code dereferences self.token_counter (accessing total_embedding_token_count)
without checking for None, which can raise AttributeError and skip usage
persistence; fix by guarding token_counter in the embedding callback(s): check
if self.token_counter is not None before reading
self.token_counter.total_embedding_token_count (use 0 or an appropriate default
when None) and apply the same guard wherever total_embedding_token_count is
accessed (e.g., the lines referencing self.token_counter at the embedding
callback and at the later usage around line 154); ensure stream_log and any
usage persistence code use the guarded/ default value instead of assuming
token_counter exists.
workers/ide_callback/tasks.py (2)

589-590: ⚠️ Potential issue | 🟡 Minor

Normalize extracted_text when present-but-None.

At Line 590, len(extracted_text) can still fail if data["extracted_text"] is explicitly None.

Proposed fix
-        extracted_text = (result_dict.get("data") or {}).get("extracted_text", "")
+        data = result_dict.get("data") or {}
+        extracted_text = data.get("extracted_text") or ""
         token_count = len(extracted_text) // 4
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@workers/ide_callback/tasks.py` around lines 589 - 590, The code assigns
extracted_text = (result_dict.get("data") or {}).get("extracted_text", "") but
if data["extracted_text"] exists and is None len(extracted_text) will raise;
normalize extracted_text to a string before computing token_count by coercing
None to "" (e.g., replace None with "" for the extracted_text variable used in
token_count calculation in tasks.py), then compute token_count =
len(extracted_text) // 4 so len() is safe.

567-572: ⚠️ Potential issue | 🟠 Major

Treat HTTP 404 from extraction persistence endpoints as terminal OSS no-op.

If cloud-only extraction endpoints are absent, persistence calls can 404 and currently flow into the outer exception path, then re-raise on Line 644. That breaks OSS-safe degradation for this callback path.

Proposed fix
@@
 def extraction_complete(
@@
-    try:
+    def _is_http_404(exc: Exception) -> bool:
+        status_code = getattr(exc, "status_code", None)
+        if status_code is None:
+            status_code = getattr(getattr(exc, "response", None), "status_code", None)
+        return status_code == 404
+
+    try:
@@
-            api.mark_extraction_error(
-                source=source,
-                file_id=file_id,
-                error=error_msg,
-                organization_id=org_id,
-            )
+            try:
+                api.mark_extraction_error(
+                    source=source,
+                    file_id=file_id,
+                    error=error_msg,
+                    organization_id=org_id,
+                )
+            except Exception as e:
+                if _is_http_404(e):
+                    logger.info(
+                        "Skipping extraction_error persistence (404 endpoint missing): source=%s file=%s",
+                        source,
+                        file_id,
+                    )
+                    return {"status": "ok", "note": "extraction callback endpoint not present (OSS)"}
+                raise
@@
-        api.mark_extraction_complete(
-            source=source,
-            file_id=file_id,
-            token_count=token_count,
-            extracted_text_path=extracted_text_path,
-            organization_id=org_id,
-        )
+        try:
+            api.mark_extraction_complete(
+                source=source,
+                file_id=file_id,
+                token_count=token_count,
+                extracted_text_path=extracted_text_path,
+                organization_id=org_id,
+            )
+        except Exception as e:
+            if _is_http_404(e):
+                logger.info(
+                    "Skipping extraction_complete persistence (404 endpoint missing): source=%s file=%s",
+                    source,
+                    file_id,
+                )
+                return {"status": "ok", "note": "extraction callback endpoint not present (OSS)"}
+            raise

Also applies to: 592-598, 620-644

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@workers/ide_callback/tasks.py` around lines 567 - 572, The persistence calls
to the API (e.g., api.mark_extraction_error called with source, file_id,
organization_id/org_id and similar calls at the other locations) must treat an
HTTP 404 from the extraction persistence endpoints as a terminal OSS no-op: wrap
each api.* persistence call (api.mark_extraction_error, the matching
api.mark_extraction_success/persist call(s) around lines ~592-598 and ~620-644)
in a try/except that catches the HTTP error type your client raises (e.g.,
HTTPError/ApiError) and if the caught exception indicates response.status_code
== 404, swallow it and return/exit this callback path instead of propagating;
for any other exception re-raise so existing error handling still applies.
Ensure you reference the same parameters (source, file_id,
org_id/organization_id, error_msg, extracted_files, checksum) when locating and
wrapping the calls.
backend/prompt_studio/prompt_studio_core_v2/views.py (2)

923-926: ⚠️ Potential issue | 🔴 Critical

prompt_studio_tool is still effectively caller-controlled on this detail route.

Lines 923-926 only change which tool is used for the profile-count check. On Line 934, self.perform_create(serializer) still persists any prompt_studio_tool the request supplied, so a caller with access to one tool can create a profile manager under another tool ID. Force the save against self.get_object() here, or make the serializer field permission-scoped.

Suggested fix
-        prompt_studio_tool = (
-            serializer.validated_data.get(ProfileManagerKeys.PROMPT_STUDIO_TOOL)
-            or self.get_object()
-        )
+        prompt_studio_tool = self.get_object()
@@
-            self.perform_create(serializer)
+            serializer.save(prompt_studio_tool=prompt_studio_tool)

Also applies to: 934-934

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/prompt_studio/prompt_studio_core_v2/views.py` around lines 923 - 926,
The code currently allows a client-supplied prompt_studio_tool to be persisted
because perform_create(serializer) uses serializer data; before calling
self.perform_create(serializer) (in the view method surrounding get_object and
serializer.validated_data), force the tool to the detail object by setting
serializer.validated_data[ProfileManagerKeys.PROMPT_STUDIO_TOOL] =
self.get_object() or call serializer.save(prompt_studio_tool=self.get_object())
so the persisted ProfileManager always uses self.get_object() instead of any
caller-supplied value; alternatively, enforce the field is
read-only/permission-scoped on the serializer to prevent client override.

99-110: ⚠️ Potential issue | 🟠 Major

Use the requested execution path to decide whether to skip this gate.

Line 109 still keys the bypass off custom_tool.single_pass_extraction_mode, so fetch_response() / bulk_fetch_response() can skip the non-single-pass lookup block just by calling those endpoints on a tool configured for single-pass mode. Pass an explicit is_single_pass flag from each caller instead of inferring it from tool settings.

Suggested direction
-def _multi_var_lookup_block_response(custom_tool, prompt_ids=None):
+def _multi_var_lookup_block_response(
+    custom_tool, *, is_single_pass: bool, prompt_ids=None
+):
@@
-    if getattr(custom_tool, "single_pass_extraction_mode", False):
+    if is_single_pass:
         return None
-        if err := _multi_var_lookup_block_response(custom_tool, prompt_ids=[prompt_id]):
+        if err := _multi_var_lookup_block_response(
+            custom_tool, is_single_pass=False, prompt_ids=[prompt_id]
+        ):
             return err
-        if err := _multi_var_lookup_block_response(custom_tool, prompt_ids=prompt_ids):
+        if err := _multi_var_lookup_block_response(
+            custom_tool, is_single_pass=False, prompt_ids=prompt_ids
+        ):
             return err
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/prompt_studio/prompt_studio_core_v2/views.py` around lines 99 - 110,
The gate function _multi_var_lookup_block_response currently checks
custom_tool.single_pass_extraction_mode directly; change its signature to accept
an explicit is_single_pass boolean (e.g., def
_multi_var_lookup_block_response(custom_tool, prompt_ids=None,
is_single_pass=False)) and use that flag to decide skipping instead of
getattr(custom_tool, "single_pass_extraction_mode", False). Update each caller
(e.g., fetch_response and bulk_fetch_response) to pass the correct execution
path by computing and passing is_single_pass from the request context/parameters
rather than relying on tool settings. Ensure all call sites of
_multi_var_lookup_block_response are updated to include the new parameter to
avoid defaulting to the wrong behavior.
backend/prompt_studio/prompt_studio_registry_v2/prompt_studio_registry_helper.py (1)

301-304: ⚠️ Potential issue | 🟠 Major

Validate lookup assignments only for prompts that are actually exportable.

Validation is still executed on the full prompt list, but NOTES/inactive prompts are skipped during export. This can fail export for prompts that will never be emitted.

🔧 Suggested fix
-        lookup_configs, lookup_error = validate_lookups_for_export(prompts)
+        lookup_validation_prompts = [
+            prompt
+            for prompt in prompts
+            if prompt.prompt_type != JsonSchemaKey.NOTES and prompt.active
+        ]
+        lookup_configs, lookup_error = validate_lookups_for_export(
+            lookup_validation_prompts
+        )
         if lookup_error:
             raise InValidCustomToolError(lookup_error)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/prompt_studio/prompt_studio_registry_v2/prompt_studio_registry_helper.py`
around lines 301 - 304, The code calls validate_lookups_for_export(prompts) over
the full prompts list which includes NOTES/inactive prompts and can block export
for items that are never emitted; change the call to run only on the subset of
exportable prompts (filter prompts to exclude NOTES/inactive entries before
calling validate_lookups_for_export) so that validate_lookups_for_export only
inspects prompts that will be exported and only raise InValidCustomToolError
when those exportable prompts fail validation.
backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py (1)

1180-1182: ⚠️ Potential issue | 🟠 Major

Single-pass lookup wiring is still missing in the legacy execution path.

This only updates build_single_pass_payload(). prompt_responder() still routes no-id flow through _execute_prompts_in_single_pass()_fetch_single_pass_response(), where tool_settings["lookup_configs"] is still not attached.

🔧 Suggested follow-up
 def _fetch_single_pass_response(...):
@@
         tool_settings[TSPKeys.SIMILARITY_TOP_K] = default_profile.similarity_top_k
+
+        lookup_configs = get_lookup_configs_for_tool(tool, prompts=prompts)
+        if lookup_configs:
+            tool_settings["lookup_configs"] = lookup_configs
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py` around
lines 1180 - 1182, The no-id legacy execution path in prompt_responder still
fails to pass lookup configs into the single-pass flow, so ensure
tool_settings["lookup_configs"] is populated for that path by calling
get_lookup_configs_for_tool(tool, prompts=prompts) and assigning the result to
tool_settings["lookup_configs"] before invoking
_execute_prompts_in_single_pass() (and consequently
_fetch_single_pass_response()); update prompt_responder to mirror the
build_single_pass_payload() change by computing lookup_configs from prompts and
attaching them to the tool_settings used in the no-id flow.
backend/prompt_studio/prompt_studio_output_manager_v2/views.py (1)

74-96: ⚠️ Potential issue | 🟠 Major

Validate tool_id format before ORM filters.

A malformed tool_id can still bubble out as a server error instead of a clean 400. Add a UUID shape guard before both query paths use tool_id.

🔧 Suggested fix
+from uuid import UUID
@@
         tool_id = request.GET.get("tool_id")
@@
         if not tool_id:
             raise ValidationError(detail=PromptOutputManagerErrorMessage.TOOL_VALIDATION)
+        try:
+            UUID(str(tool_id))
+        except (TypeError, ValueError):
+            raise ValidationError(
+                detail=PromptOutputManagerErrorMessage.TOOL_VALIDATION
+            )
@@
         tool_id = request.GET.get("tool_id")
@@
         if not tool_id:
             raise ValidationError(detail=tool_validation_message)
+        try:
+            UUID(str(tool_id))
+        except (TypeError, ValueError):
+            raise ValidationError(detail=tool_validation_message)

Please verify with an API test that tool_id=not-a-uuid returns HTTP 400 on both endpoints.

Also applies to: 127-138

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/prompt_studio/prompt_studio_output_manager_v2/views.py` around lines
74 - 96, Add a UUID-shape guard that validates tool_id before any ORM usage:
attempt to parse request.GET.get("tool_id") with uuid.UUID(...) in a try/except
and if parsing fails raise
ValidationError(detail=PromptOutputManagerErrorMessage.TOOL_VALIDATION). Apply
this check in the current handler before building prompt_keys and before the
other query path referenced in the comment (the block using
ToolStudioPrompt.objects.filter and the similar block at lines 127-138), so no
malformed tool_id reaches the ORM; keep using the same ValidationError and
message to ensure the endpoint returns HTTP 400.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/prompt_studio/lookup_utils.py`:
- Around line 18-38: The ImportError guard misses the parent package name, so
when pluggable_apps is absent the exception is re-raised; update
_CLOUD_LOOKUP_MODULES to include "pluggable_apps" (in addition to the existing
entries) so the except block will treat missing the parent package as expected
and set LOOKUPS_AVAILABLE = False; ensure this change covers the import block
that imports execution/output_enrichment/staleness/validation and
LookupOutputResult (symbols: _CLOUD_LOOKUP_MODULES, _execution,
_output_enrichment, _staleness, _validation, _LookupOutputResult,
LOOKUPS_AVAILABLE, and the ImportError handling using e.name).

---

Duplicate comments:
In `@backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py`:
- Around line 1180-1182: The no-id legacy execution path in prompt_responder
still fails to pass lookup configs into the single-pass flow, so ensure
tool_settings["lookup_configs"] is populated for that path by calling
get_lookup_configs_for_tool(tool, prompts=prompts) and assigning the result to
tool_settings["lookup_configs"] before invoking
_execute_prompts_in_single_pass() (and consequently
_fetch_single_pass_response()); update prompt_responder to mirror the
build_single_pass_payload() change by computing lookup_configs from prompts and
attaching them to the tool_settings used in the no-id flow.

In `@backend/prompt_studio/prompt_studio_core_v2/views.py`:
- Around line 923-926: The code currently allows a client-supplied
prompt_studio_tool to be persisted because perform_create(serializer) uses
serializer data; before calling self.perform_create(serializer) (in the view
method surrounding get_object and serializer.validated_data), force the tool to
the detail object by setting
serializer.validated_data[ProfileManagerKeys.PROMPT_STUDIO_TOOL] =
self.get_object() or call serializer.save(prompt_studio_tool=self.get_object())
so the persisted ProfileManager always uses self.get_object() instead of any
caller-supplied value; alternatively, enforce the field is
read-only/permission-scoped on the serializer to prevent client override.
- Around line 99-110: The gate function _multi_var_lookup_block_response
currently checks custom_tool.single_pass_extraction_mode directly; change its
signature to accept an explicit is_single_pass boolean (e.g., def
_multi_var_lookup_block_response(custom_tool, prompt_ids=None,
is_single_pass=False)) and use that flag to decide skipping instead of
getattr(custom_tool, "single_pass_extraction_mode", False). Update each caller
(e.g., fetch_response and bulk_fetch_response) to pass the correct execution
path by computing and passing is_single_pass from the request context/parameters
rather than relying on tool settings. Ensure all call sites of
_multi_var_lookup_block_response are updated to include the new parameter to
avoid defaulting to the wrong behavior.

In `@backend/prompt_studio/prompt_studio_output_manager_v2/views.py`:
- Around line 74-96: Add a UUID-shape guard that validates tool_id before any
ORM usage: attempt to parse request.GET.get("tool_id") with uuid.UUID(...) in a
try/except and if parsing fails raise
ValidationError(detail=PromptOutputManagerErrorMessage.TOOL_VALIDATION). Apply
this check in the current handler before building prompt_keys and before the
other query path referenced in the comment (the block using
ToolStudioPrompt.objects.filter and the similar block at lines 127-138), so no
malformed tool_id reaches the ORM; keep using the same ValidationError and
message to ensure the endpoint returns HTTP 400.

In
`@backend/prompt_studio/prompt_studio_registry_v2/prompt_studio_registry_helper.py`:
- Around line 301-304: The code calls validate_lookups_for_export(prompts) over
the full prompts list which includes NOTES/inactive prompts and can block export
for items that are never emitted; change the call to run only on the subset of
exportable prompts (filter prompts to exclude NOTES/inactive entries before
calling validate_lookups_for_export) so that validate_lookups_for_export only
inspects prompts that will be exported and only raise InValidCustomToolError
when those exportable prompts fail validation.

In `@backend/usage_v2/internal_views.py`:
- Around line 164-193: The loop that constructs Usage objects from records (the
for r in records -> Usage(...) block) is too permissive and can mask producer
bugs; add explicit validation before building each Usage: verify required fields
(e.g., workflow_id, execution_id, usage_type, adapter_instance_id or run_id as
applicable) are present and of the expected type, coerce/validate numeric fields
(embedding_tokens, prompt_tokens, completion_tokens, total_tokens as ints;
cost_in_dollars as float), ensure model_name is non-empty when usage_type ==
"llm", and enforce the llm_usage_reason None/coercion rule; for records that
fail validation, log a clear error including the offending record and either
skip the record or collect/report them so the batch write isn't aborted by a
single malformed row, then only append validated Usage instances to
usage_objects.

In `@unstract/sdk1/src/unstract/sdk1/usage_handler.py`:
- Around line 108-112: The code dereferences self.token_counter (accessing
total_embedding_token_count) without checking for None, which can raise
AttributeError and skip usage persistence; fix by guarding token_counter in the
embedding callback(s): check if self.token_counter is not None before reading
self.token_counter.total_embedding_token_count (use 0 or an appropriate default
when None) and apply the same guard wherever total_embedding_token_count is
accessed (e.g., the lines referencing self.token_counter at the embedding
callback and at the later usage around line 154); ensure stream_log and any
usage persistence code use the guarded/ default value instead of assuming
token_counter exists.

In `@workers/executor/tasks.py`:
- Around line 113-130: The current flush path in tasks.py logs failures from
bulk_create_usage but swallows them, risking dropped billing rows; modify the
failure handling inside the block that calls bulk_create_usage so that on a
non-ok result or on exception you propagate an error to the caller (either by
re-raising the caught exception or raising a RecoverableError) instead of only
logging, so the orchestration can retry; keep the existing logger.error messages
(including run_id and organization_id) but follow them with a raise to fail the
task and preserve usage_records/context for retry handling.

In `@workers/ide_callback/tasks.py`:
- Around line 589-590: The code assigns extracted_text =
(result_dict.get("data") or {}).get("extracted_text", "") but if
data["extracted_text"] exists and is None len(extracted_text) will raise;
normalize extracted_text to a string before computing token_count by coercing
None to "" (e.g., replace None with "" for the extracted_text variable used in
token_count calculation in tasks.py), then compute token_count =
len(extracted_text) // 4 so len() is safe.
- Around line 567-572: The persistence calls to the API (e.g.,
api.mark_extraction_error called with source, file_id, organization_id/org_id
and similar calls at the other locations) must treat an HTTP 404 from the
extraction persistence endpoints as a terminal OSS no-op: wrap each api.*
persistence call (api.mark_extraction_error, the matching
api.mark_extraction_success/persist call(s) around lines ~592-598 and ~620-644)
in a try/except that catches the HTTP error type your client raises (e.g.,
HTTPError/ApiError) and if the caught exception indicates response.status_code
== 404, swallow it and return/exit this callback path instead of propagating;
for any other exception re-raise so existing error handling still applies.
Ensure you reference the same parameters (source, file_id,
org_id/organization_id, error_msg, extracted_files, checksum) when locating and
wrapping the calls.
🪄 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: CHILL

Plan: Pro

Run ID: ce8433f8-3196-4387-824b-9de175c39622

📥 Commits

Reviewing files that changed from the base of the PR and between f9d3844 and 2fd9cf5.

📒 Files selected for processing (20)
  • .gitignore
  • backend/prompt_studio/lookup_utils.py
  • backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py
  • backend/prompt_studio/prompt_studio_core_v2/views.py
  • backend/prompt_studio/prompt_studio_output_manager_v2/output_manager_helper.py
  • backend/prompt_studio/prompt_studio_output_manager_v2/serializers.py
  • backend/prompt_studio/prompt_studio_output_manager_v2/views.py
  • backend/prompt_studio/prompt_studio_registry_v2/prompt_studio_registry_helper.py
  • backend/usage_v2/internal_views.py
  • backend/usage_v2/migrations/0006_alter_usage_status_and_more.py
  • backend/usage_v2/models.py
  • frontend/src/components/custom-tools/prompt-card/PromptOutput.jsx
  • frontend/src/hooks/usePromptOutput.js
  • unstract/sdk1/src/unstract/sdk1/llm.py
  • unstract/sdk1/src/unstract/sdk1/usage_handler.py
  • workers/executor/executors/legacy_executor.py
  • workers/executor/executors/plugins/protocols.py
  • workers/executor/tasks.py
  • workers/ide_callback/tasks.py
  • workers/shared/clients/usage_client.py
✅ Files skipped from review due to trivial changes (2)
  • backend/prompt_studio/prompt_studio_output_manager_v2/output_manager_helper.py
  • workers/executor/executors/legacy_executor.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • .gitignore

Comment thread backend/prompt_studio/lookup_utils.py
Five threads on PR #1929 raised against the latest disposition push:

* prompt_studio_core_v2/views.py: drop the ``single_pass_extraction_mode``
  bypass from ``_multi_var_lookup_block_response`` — fetch_response /
  bulk_fetch_response are always non-SP, so the tool-setting check just
  let multi-var lookups slip past the gate when the tool happened to be
  configured for SP.

* prompt_studio_registry_helper.py: filter out NOTES + inactive prompts
  *before* calling ``validate_lookups_for_export`` so an incomplete
  lookup on a non-exportable prompt no longer fails the whole export.

* unstract/sdk1/usage_handler.py: guard ``self.token_counter is None``
  in the embedding-end branch — degrade with a warning instead of an
  AttributeError on early callbacks.

* workers/executor/legacy_executor.py: move the ``outcome.usage_records``
  / ``outcome.llm_metrics`` access inside the ``try`` so plugin contract
  drift hits the same graceful-degrade branch as a thrown ``run_with_metrics``.

* backend/prompt_studio/lookup_utils.py: include ``pluggable_apps`` itself
  in ``_CLOUD_LOOKUP_MODULES``. Pure OSS images don't have the parent
  package, so ``ImportError.name`` surfaces as ``"pluggable_apps"`` and
  the previous filter re-raised instead of setting LOOKUPS_AVAILABLE=False.
@github-actions
Copy link
Copy Markdown
Contributor

Frontend Lint Report (Biome)

All checks passed! No linting or formatting issues found.

@chandrasekharan-zipstack
Copy link
Copy Markdown
Contributor Author

Round-2 disposition

OSS commit ba6c32ab0 addresses the five threads raised after the previous disposition.

Reviewer Path:Line Resolution
@coderabbitai prompt_studio_core_v2/views.py:123 (single-pass gate keys off tool setting) ba6c32ab0 — drop the single_pass_extraction_mode early-out from _multi_var_lookup_block_response. Both call sites (fetch_response, bulk_fetch_response) are non-SP execution paths; the SP entry point doesn't go through this helper, so the tool-setting check was a pure bypass
@coderabbitai prompt_studio_registry_helper.py:304 (validate_lookups_for_export runs before NOTES/inactive drop) ba6c32ab0 — filter to exportable prompts (prompt_type != NOTES and active) before passing into validate_lookups_for_export
@coderabbitai usage_handler.py:154 (token_counter not guarded) ba6c32ab0 — explicit if self.token_counter is None: warning + return before reading total_embedding_token_count / calling reset_counts
@greptile-apps legacy_executor.py:2099 (outcome.usage_records outside try) ba6c32ab0 — moved both attribute accesses inside the try so plugin contract drift hits the same graceful-degrade branch as a run_with_metrics exception
@coderabbitai lookup_utils.py:38 (_CLOUD_LOOKUP_MODULES misses parent package) ba6c32ab0 — added "pluggable_apps" itself to the set so pure OSS images (where ImportError.name == "pluggable_apps") set LOOKUPS_AVAILABLE=False instead of re-raising

🤖 Generated with Claude Code

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

Tip

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

@chandrasekharan-zipstack — thank you for the clean round-2 accounting. All five items check out:

Item Verdict
views.py:123_multi_var_lookup_block_response no longer has the SP bypass ✅ Confirmed
prompt_studio_registry_helper.py:304 — exportable-prompt pre-filter before validate_lookups_for_export ✅ Confirmed
usage_handler.py:154 — explicit token_counter is None early-return guard ✅ Confirmed
legacy_executor.py:2099outcome.usage_records accesses moved inside try ✅ Confirmed
lookup_utils.py:38"pluggable_apps" added to _CLOUD_LOOKUP_MODULES sentinel set ✅ Confirmed

The "pluggable_apps" fix in particular closes a subtle gap — without it, a fresh OSS image missing the entire pluggable_apps namespace would re-raise rather than gracefully degrade to LOOKUPS_AVAILABLE = False, which would have caused hard startup failures in pure OSS deployments.

I'll resolve all my open threads now.

[resolve]

@github-actions
Copy link
Copy Markdown
Contributor

Test Results

Summary
  • Runner Tests: 11 passed, 0 failed (11 total)
  • SDK1 Tests: 230 passed, 0 failed (230 total)

Runner Tests - Full Report
filepath function $$\textcolor{#23d18b}{\tt{passed}}$$ SUBTOTAL
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_logs}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_cleanup}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_cleanup\_skip}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_client\_init}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_container\_run\_config}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_container\_run\_config\_without\_mount}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_run\_container}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image\_for\_sidecar}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_sidecar\_container}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{TOTAL}}$$ $$\textcolor{#23d18b}{\tt{11}}$$ $$\textcolor{#23d18b}{\tt{11}}$$
SDK1 Tests - Full Report
filepath function $$\textcolor{#23d18b}{\tt{passed}}$$ SUBTOTAL
$$\textcolor{#23d18b}{\tt{tests/file\_storage/test\_impl\_rm.py}}$$ $$\textcolor{#23d18b}{\tt{TestRmHappyPath.test\_bulk\_delete\_succeeds}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/file\_storage/test\_impl\_rm.py}}$$ $$\textcolor{#23d18b}{\tt{TestRmFallback.test\_missing\_md5\_triggers\_individual\_delete\_via\_rm\_file}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/file\_storage/test\_impl\_rm.py}}$$ $$\textcolor{#23d18b}{\tt{TestRmFallback.test\_fallback\_continues\_on\_per\_file\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/file\_storage/test\_impl\_rm.py}}$$ $$\textcolor{#23d18b}{\tt{TestRmFallback.test\_fallback\_swallows\_rmdir\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/file\_storage/test\_impl\_rm.py}}$$ $$\textcolor{#23d18b}{\tt{TestRmFallback.test\_non\_md5\_error\_propagates}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/file\_storage/test\_impl\_rm.py}}$$ $$\textcolor{#23d18b}{\tt{TestRmFallback.test\_md5\_error\_without\_recursive\_propagates}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/file\_storage/test\_impl\_rm.py}}$$ $$\textcolor{#23d18b}{\tt{TestFallbackDoesNotReenterBulkDelete.test\_only\_singular\_delete\_called}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/patches/test\_litellm\_cohere\_timeout.py}}$$ $$\textcolor{#23d18b}{\tt{TestPatchedEmbeddingSyncTimeoutForwarding.test\_timeout\_passed\_to\_client\_post}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/patches/test\_litellm\_cohere\_timeout.py}}$$ $$\textcolor{#23d18b}{\tt{TestPatchedEmbeddingSyncTimeoutForwarding.test\_none\_timeout\_passed\_to\_client\_post}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/patches/test\_litellm\_cohere\_timeout.py}}$$ $$\textcolor{#23d18b}{\tt{TestPatchedEmbeddingSyncTimeoutForwarding.test\_httpx\_timeout\_object\_forwarded}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/patches/test\_litellm\_cohere\_timeout.py}}$$ $$\textcolor{#23d18b}{\tt{TestMonkeyPatchApplied.test\_cohere\_handler\_patched}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/patches/test\_litellm\_cohere\_timeout.py}}$$ $$\textcolor{#23d18b}{\tt{TestMonkeyPatchApplied.test\_bedrock\_handler\_patched}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/patches/test\_litellm\_cohere\_timeout.py}}$$ $$\textcolor{#23d18b}{\tt{TestMonkeyPatchApplied.test\_patch\_module\_loaded\_via\_embedding\_import}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_round\_trip\_serialization}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_json\_serializable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_enum\_values\_normalized}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_string\_values\_accepted}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_auto\_generates\_request\_id}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_explicit\_request\_id\_preserved}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_optional\_organization\_id}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_empty\_executor\_params\_default}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_complex\_executor\_params}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_validation\_rejects\_empty\_required\_fields}}$$ $$\textcolor{#23d18b}{\tt{4}}$$ $$\textcolor{#23d18b}{\tt{4}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_all\_operations\_accepted}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionContext.test\_from\_dict\_missing\_optional\_fields}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_success\_round\_trip}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_failure\_round\_trip}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_json\_serializable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_failure\_requires\_error\_message}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_success\_allows\_no\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_success\_rejects\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_failure\_factory}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_failure\_factory\_no\_metadata}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_error\_none\_in\_success\_dict}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_error\_in\_failure\_dict}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_default\_empty\_dicts}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_from\_dict\_missing\_optional\_fields}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_response\_contract\_extract}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_response\_contract\_index}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionResult.test\_response\_contract\_answer\_prompt}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestBaseExecutor.test\_cannot\_instantiate\_abstract}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestBaseExecutor.test\_concrete\_subclass\_works}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestBaseExecutor.test\_execute\_returns\_result}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_register\_and\_get}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_get\_returns\_fresh\_instance}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_register\_as\_decorator}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_list\_executors}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_list\_executors\_empty}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_get\_unknown\_raises\_key\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_get\_unknown\_lists\_available}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_duplicate\_name\_raises\_value\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_register\_non\_subclass\_raises\_type\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_register\_non\_class\_raises\_type\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_clear}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorRegistry.test\_execute\_through\_registry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionOrchestrator.test\_dispatches\_to\_correct\_executor}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionOrchestrator.test\_unknown\_executor\_returns\_failure}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionOrchestrator.test\_executor\_exception\_returns\_failure}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionOrchestrator.test\_exception\_result\_has\_elapsed\_metadata}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionOrchestrator.test\_successful\_result\_passed\_through}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionOrchestrator.test\_executor\_returning\_failure\_is\_not\_wrapped}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_sends\_task\_and\_returns\_result}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_uses\_default\_timeout}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_timeout\_from\_env}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_explicit\_timeout\_overrides\_env}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_timeout\_returns\_failure}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_generic\_exception\_returns\_failure}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_async\_returns\_task\_id}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_no\_app\_raises\_value\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_async\_no\_app\_raises\_value\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_failure\_result\_from\_executor}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_context\_serialized\_correctly}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_sends\_link\_and\_link\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_success\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_error\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_no\_callbacks}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_returns\_async\_result}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_no\_app\_raises\_value\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_context\_serialized}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_custom\_task\_id}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutionDispatcher.test\_dispatch\_with\_callback\_no\_task\_id\_omits\_kwarg}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_platform\_api\_key\_returned}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_platform\_api\_key\_missing\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_other\_env\_var\_from\_environ}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_missing\_env\_var\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_empty\_env\_var\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_stream\_log\_routes\_to\_logging}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_stream\_log\_respects\_level}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_stream\_error\_and\_exit\_raises\_sdk\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_execution.py}}$$ $$\textcolor{#23d18b}{\tt{TestExecutorToolShim.test\_stream\_error\_and\_exit\_wraps\_original}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_model\_prefixes\_when\_missing}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_model\_does\_not\_double\_prefix}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_model\_blank\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_thinking\_disabled\_by\_default}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_excludes\_control\_fields\_from\_model}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_thinking\_enabled\_with\_budget}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_thinking\_overrides\_user\_temperature}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_thinking\_enabled\_without\_budget\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_thinking\_budget\_tokens\_invalid\_type\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_thinking\_budget\_tokens\_too\_small\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_preserves\_existing\_thinking\_config}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_validate\_does\_not\_mutate\_input}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_thinking\_controls\_not\_pydantic\_fields}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_api\_key\_is\_required}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_adapter\_identity}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_schema\_required\_fields}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_schema\_enable\_thinking\_default\_false}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_adapter.py}}$$ $$\textcolor{#23d18b}{\tt{test\_schema\_budget\_tokens\_conditional}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_adapter\_registration}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_get\_id\_format}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_get\_adapter\_type}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_get\_name}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_get\_provider}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_json\_schema\_loads}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_json\_schema\_required\_fields}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_json\_schema\_no\_batch\_size\_default}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_json\_schema\_api\_key\_password\_format}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_json\_schema\_model\_default}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_model\_adds\_prefix}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_model\_idempotent}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_model\_does\_not\_mutate\_input}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_does\_not\_mutate\_input}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_model\_empty\_string\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_model\_whitespace\_only\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_model\_none\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_model\_missing\_key\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_empty\_model\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_none\_model\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_missing\_api\_key\_raises}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_calls\_validate\_model}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_embed\_batch\_size\_none\_by\_default}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_embed\_batch\_size\_preserved}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_strips\_extra\_fields}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_validate\_includes\_base\_fields}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_gemini\_embedding.py}}$$ $$\textcolor{#23d18b}{\tt{TestGeminiEmbeddingAdapter.test\_metadata}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatFromLlm.test\_from\_llm\_reuses\_llm\_instance}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatFromLlm.test\_from\_llm\_returns\_llmcompat\_instance}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatFromLlm.test\_from\_llm\_sets\_model\_name}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatFromLlm.test\_from\_llm\_does\_not\_call\_init}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_complete\_delegates\_to\_llm}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_chat\_delegates\_to\_llm\_complete}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_chat\_forwards\_kwargs\_to\_llm}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_complete\_forwards\_kwargs\_to\_llm}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_acomplete\_delegates\_to\_llm}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_achat\_delegates\_to\_llm\_acomplete}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_stream\_chat\_not\_implemented}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_stream\_complete\_not\_implemented}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_astream\_chat\_not\_implemented}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_astream\_complete\_not\_implemented}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_metadata\_returns\_emulated\_type}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_get\_model\_name\_delegates}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_get\_metrics\_delegates}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestLLMCompatDelegation.test\_test\_connection\_delegates}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestEmulatedTypes.test\_message\_role\_values}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestEmulatedTypes.test\_chat\_message\_defaults}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestEmulatedTypes.test\_chat\_response\_message\_access}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestEmulatedTypes.test\_completion\_response\_text}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestEmulatedTypes.test\_llm\_metadata\_defaults}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestMessagesToPrompt.test\_single\_user\_message}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestMessagesToPrompt.test\_none\_content\_becomes\_empty\_string}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestMessagesToPrompt.test\_preserves\_all\_messages}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestMessagesToPrompt.test\_multi\_turn\_conversation}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestMessagesToPrompt.test\_empty\_messages\_returns\_empty\_string}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_llm\_compat.py}}$$ $$\textcolor{#23d18b}{\tt{TestMessagesToPrompt.test\_string\_role\_fallback}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_success\_on\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retry\_on\_connection\_error}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_non\_retryable\_http\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retryable\_http\_errors}}$$ $$\textcolor{#23d18b}{\tt{3}}$$ $$\textcolor{#23d18b}{\tt{3}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_post\_method\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retry\_logging}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_success\_on\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_retry\_on\_errors}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_wrapper\_methods\_retry}}$$ $$\textcolor{#23d18b}{\tt{4}}$$ $$\textcolor{#23d18b}{\tt{4}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_connection\_error\_is\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_timeout\_is\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_retryable\_status\_codes}}$$ $$\textcolor{#23d18b}{\tt{3}}$$ $$\textcolor{#23d18b}{\tt{3}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_non\_retryable\_status\_codes}}$$ $$\textcolor{#23d18b}{\tt{5}}$$ $$\textcolor{#23d18b}{\tt{5}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_without\_response}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_os\_error\_retryable\_errno}}$$ $$\textcolor{#23d18b}{\tt{5}}$$ $$\textcolor{#23d18b}{\tt{5}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_os\_error\_non\_retryable\_errno}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_other\_exception\_not\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_exponential\_backoff\_without\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_exponential\_backoff\_with\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_max\_delay\_cap}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_max\_delay\_cap\_with\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_successful\_call\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_retry\_after\_transient\_failure}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_max\_retries\_exceeded}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_retry\_with\_custom\_predicate}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_no\_retry\_with\_predicate\_false}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_exception\_not\_in\_tuple\_not\_retried}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_default\_configuration}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_environment\_variable\_configuration}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_max\_retries}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_base\_delay}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_multiplier}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_jitter\_values}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_custom\_exceptions\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_custom\_predicate\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_both\_exceptions\_and\_predicate}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_exceptions\_match\_but\_predicate\_false}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_retry\_platform\_service\_call\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_retry\_prompt\_service\_call\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_platform\_service\_decorator\_retries\_on\_connection\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_prompt\_service\_decorator\_retries\_on\_timeout}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_warning\_logged\_on\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_info\_logged\_on\_success\_after\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_exception\_logged\_on\_giving\_up}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{TOTAL}}$$ $$\textcolor{#23d18b}{\tt{230}}$$ $$\textcolor{#23d18b}{\tt{230}}$$

@sonarqubecloud
Copy link
Copy Markdown

@athul-rs
Copy link
Copy Markdown
Contributor

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

)


class UsageBatchCreateView(APIView):
Copy link
Copy Markdown
Contributor

@harini-venkataraman harini-venkataraman Apr 30, 2026

Choose a reason for hiding this comment

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

The UsageBatchCreateView has no explicit authentication or permission class. While the existing internal views in this file may rely on some middleware/URL-level gating, this is a
billing-critical write endpoint that bulk-creates usage records. If the internal API is accessible without auth (e.g., via service mesh), any caller can inject arbitrary usage rows.

Suggestion: Explicitly add permission_classes or document which middleware gates this endpoint. At minimum add a comment clarifying the auth contract.

)

usage_objects = []
for r in records:
Copy link
Copy Markdown
Contributor

@harini-venkataraman harini-venkataraman Apr 30, 2026

Choose a reason for hiding this comment

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

The records list is iterated and each dict's values are passed directly to Usage() model construction via .get() with no schema validation. A malformed record (wrong types, missing keys,
extra keys) will only surface as a DB-level error caught by the broad except Exception.

Suggestion: Add a serializer or at minimum validate required fields before constructing model instances. This is a billing-critical table — silent failures or corrupt data would be costly.

prompt_tokens=usage_data.get("prompt_tokens", 0),
completion_tokens=usage_data.get("completion_tokens", 0),
total_tokens=usage_data.get("total_tokens", 0),
prompt_tokens = usage_data.get("prompt_tokens", 0)
Copy link
Copy Markdown
Contributor

@harini-venkataraman harini-venkataraman Apr 30, 2026

Choose a reason for hiding this comment

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

The old _record_usage called token_counter(model=model, messages=messages) from litellm to independently count prompt tokens. The new version blindly trusts usage_data.get("prompt_tokens",
0) from the API response. Some providers return 0 or omit token counts. This silently records 0 tokens where previously litellm would compute an accurate count.

Suggestion: Add a fallback: if usage_data returns 0 for prompt_tokens but messages are non-empty, use litellm.token_counter() as a fallback.

ProfileManagerKeys.PROMPT_STUDIO_TOOL
]
prompt_studio_tool = (
serializer.validated_data.get(ProfileManagerKeys.PROMPT_STUDIO_TOOL)
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.

This changes a KeyError (immediate failure) to a silent fallback to self.get_object(). If the serializer is supposed to always include the tool, this masks bugs. If the field is genuinely
optional now, the serializer's field definition should reflect that.

# callers get the intended 400.
raise ValidationError(detail=PromptOutputManagerErrorMessage.TOOL_VALIDATION)

prompt_keys = [k.strip() for k in keys_param.split(",") if k.strip()]
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.

The new latest_outputs_by_keys endpoint accepts a comma-separated prompt_keys param with no limit on how many keys can be requested. A request with hundreds of keys would generate a large
SQL query and potentially large response.

prompt_keys = [k.strip() for k in keys_param.split(",") if k.strip()]

Suggestion: Cap prompt_keys to a reasonable maximum (e.g., 100) and return 400 if exceeded.

}, [scrollToBottom]);

// Handle scrollTo query param for cross-linking from Lookup Studio
const UUID_RE =
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.

The regex is declared inside the component body, so it's recreated on every render:

Suggestion: Move UUID_RE outside the component as a module-level constant.

level=LogLevel.ERROR,
)

def _run_lookup_enrichment(
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.

REFACTOR : Can move look-up related helpers to a separate directory for maintainability and logical separation. By this way we can prevent maintaining large files.

prompt_name,
)
return
if output_type != PSKeys.JSON:
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.

Previously, webhook postprocessing lived inside answer_prompt.handle_json and ran for any output type (the code was inside the JSON handler but after the parse). Now it's explicitly gated
to output_type == PSKeys.JSON. This is a behavioral change for any user who has webhooks configured on non-JSON prompts — their webhooks will silently stop firing.

Suggestion: This should be called out prominently in the PR description and migration notes. Consider a deprecation warning period rather than silently breaking existing webhook
configurations.

names = get_multi_var_lookups_for_tool(custom_tool, prompt_ids=prompt_ids)
if not names:
return None
return Response(
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.

_multi_var_lookup_block_response returns {"error": "..."} but DRF's standard pattern uses {"detail": "..."}. Other endpoints in this file use both. Lets standardize on one.

return {
output,
enriched: buildEnrichedFromBundle(output, bundle, displayPromptResult),
hasEnriched: true,
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.

hasEnriched Is Always true in Default Profile Path. This means the enriched toggle may appear even when there's no enrichment data. It should be hasEnriched: bundle != null.

# paths; guard against AttributeError so a benign empty-data response
# doesn't escalate to a generic "ERROR" callback.
extracted_text = (result_dict.get("data") or {}).get("extracted_text", "")
token_count = len(extracted_text) // 4
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.

This character-count-based estimation (÷4) is a very rough approximation and can be significantly off for non-English text or code. Consider using litellm.token_counter since litellm is
already a dependency.

from django.db import migrations, models


class Migration(migrations.Migration):
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.

Migration 0004 adds the status field without choices, and 0006 immediately alters it to add choices. Since this is a new feature branch, these could be squashed into one migration to avoid
unnecessary ALTER TABLE operations on deployment.

// OSS: plugin may not exist; cloud: surface unexpected chunk-load
// failures so they don't degrade silently to OSS-mode behaviour.
// eslint-disable-next-line no-console
console.warn("[PromptOutput] LookupOutputTabs unavailable:", error);
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.

In OSS builds, this will emit console warnings on every page load for every user. These cloud-only plugin imports should use a silent catch (like other similar patterns in this PR do with
empty catch {}).

if vector_db:
vector_db.close()

def _init_llm_and_retrieval(
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.

If vector_db_cls() throws, llm and embedding are created but the except re-raises as LegacyExecutorError. The caller's try/finally with _flush_per_prompt_metrics (which calls
vector_db.close() and flushes usage) is never entered because the destructuring assignment llm, embedding, vector_db = self._init_llm_and_retrieval(...) fails. The llm._pending_usage
records from construction-time calls (if any) are silently dropped.

Suggestion: Flush llm.flush_pending_usage() in the except block before re-raising, and extend self._usage_records with whatever was collected.

ws_room = cb.get("ws_room", "")
ws_event = cb.get("ws_event", "")

api = _get_extraction_client()
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.

If BaseAPIClient manages HTTP sessions, ExtractionAPIClient leaks connections on every Celery task invocation.
Compare with UsageAPIClient in workers/executor/tasks.py which correctly uses with:
with UsageAPIClient(config) as usage_client:

ws_room = cb.get("ws_room", "")
ws_event = cb.get("ws_event", "")

api = _get_extraction_client()
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.

If BaseAPIClient manages HTTP sessions, ExtractionAPIClient leaks connections on every Celery task invocation.
Compare with UsageAPIClient in workers/executor/tasks.py which correctly uses with:
with UsageAPIClient(config) as usage_client:

if embedding.callback_manager is not None:
for handler in embedding.callback_manager.handlers:
if hasattr(handler, "flush_pending_usage"):
self._usage_records.extend(handler.flush_pending_usage())
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.

If one handler's flush_pending_usage() throws, all subsequent handlers' records are lost. This loop should be guarded per-handler:

for handler in embedding.callback_manager.handlers:
     if hasattr(handler, "flush_pending_usage"):
         try:
             self._usage_records.extend(handler.flush_pending_usage())
         except Exception:
             logger.warning("Failed to flush usage from handler", exc_info=True)

)
# Inside the try so a missing/renamed attribute on the outcome
# (plugin contract drift) hits the same graceful-degrade branch.
self._usage_records.extend(outcome.usage_records)
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.

If run_with_metrics makes an LLM call internally and then crashes (e.g., on post-processing the result), the lookup LLM's token consumption happened but the usage records are trapped
inside the plugin and never flushed. This is billing data loss.

Suggestion: The plugin protocol should expose a way to recover partial usage even on failure, or the executor should pass its _usage_records list to the plugin so records are appended in
real-time.

self._metrics = new_metrics

# Stamp timing onto the most recent pending usage record
pending = getattr(self, "_pending_usage", [])
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.

This always stamps the last record in _pending_usage. If a decorated method makes multiple LLM calls (e.g., retry logic inside a single complete() call), only the last call gets timing.
Earlier calls get execution_time_ms = None in the DB, which is misleading — it looks like the data wasn't captured rather than being an intermediate call.

# unused choice entries on OSS.

LLM_USAGE_REASON_CHOICES: list[tuple[str, str]] = [
("extraction", "Extraction"),
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.

Any code that references LLMUsageReason.EXTRACTION (the enum member) will now fail with ImportError or AttributeError. If any serializer, filter, or admin code references the old enum,
it's broken.

Suggestion: Grep the codebase for LLMUsageReason usages and verify none remain. Consider keeping the enum class and deriving the choices list from it.

}
const useDefaultProfile = activeKey === "0" && !isSimplePromptStudio;
const { output, enriched } = useDefaultProfile
? buildDefaultProfileOutputs(data)
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.

buildDefaultProfileOutputs, buildPerPromptOutput, and buildSelectedProfileOutputs are defined inside the component body and recreated on every render. They capture selectedProfile,
displayPromptResult, and other values via closure. Since they're only used inside fetchCombinedOutput (an async callback), they should be wrapped in useCallback or moved inside the
fetchCombinedOutput body to make the lifecycle clearer.


// Extend Prompt Studio active state to include /lookups paths
if (lookupStudioEnabled && isUnstract) {
const psItem = data[0]?.subMenu?.find((el) => el.id === 1.1);
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.

This directly mutates the data array's child object. If data comes from state or a shared reference, this mutation can cause stale renders or bugs in other components reading the same
data. Use spread/clone instead.

Copy link
Copy Markdown
Contributor

@athul-rs athul-rs left a comment

Choose a reason for hiding this comment

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

A few nits from a follow-up pass — non-blocking.

Comment on lines +925 to +926
serializer.validated_data.get(ProfileManagerKeys.PROMPT_STUDIO_TOOL)
or self.get_object()
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.

nit: validated_data.get(...) or self.get_object() only patches the local variable. serializer.validated_data still lacks prompt_studio_tool, and ProfileManager.prompt_studio_tool is null=True, so if the field is ever omitted from the request perform_create() could persist a row with prompt_studio_tool=NULL — orphaned from filter(prompt_studio_tool=...) queries. Either make the serializer field required, or write the resolved tool back into validated_data before perform_create.

)
# Inside the try so a missing/renamed attribute on the outcome
# (plugin contract drift) hits the same graceful-degrade branch.
self._usage_records.extend(outcome.usage_records)
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.

nit: self._usage_records.extend(outcome.usage_records) lives inside the try, so any LLM usage already accumulated by the lookup plugin is dropped if run_with_metrics raises after partial work — billing loss. _run_challenge_if_enabled in this same file already establishes the right pattern (flush in finally). Consider moving the extend into a finally block guarded by if outcome is not None.

)
return
if output_type != PSKeys.JSON:
# The pre-refactor behaviour fired the webhook regardless of
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.

nit: this comment is inaccurate — pre-refactor, the webhook was invoked from inside AnswerPromptService.handle_json, which the executor only called from the elif output_type == PSKeys.JSON: branch. So webhooks were never fired for non-JSON outputs. The new restriction matches old behaviour rather than tightening it; worth rewording so future readers don't assume a regression surface.

@athul-rs
Copy link
Copy Markdown
Contributor

nit (couldn't anchor inline — line is unchanged context): the docstring at unstract/sdk1/src/unstract/sdk1/usage_handler.py#L100 still says "Push the usage of Embedding to platform service." but on_event_end no longer pushes — it appends to self._pending_usage for deferred flushing. Worth updating the docstring to match the new contract (e.g. "Record embedding usage for deferred flush.").

Copy link
Copy Markdown
Contributor

@athul-rs athul-rs left a comment

Choose a reason for hiding this comment

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

@chandrasekharan-zipstack please chekc the NIt comments added

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.

4 participants