Skip to content

feat(llm): Introduce type safety checks - #1089

Open
Prajna1999 wants to merge 2 commits into
mainfrom
chore/type-safety-llm-calls
Open

feat(llm): Introduce type safety checks#1089
Prajna1999 wants to merge 2 commits into
mainfrom
chore/type-safety-llm-calls

Conversation

@Prajna1999

@Prajna1999 Prajna1999 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Issue

Closes #PLEASE_TYPE_ISSUE_NUMBER

Summary

  • Before: No type safety in LLM calls.
  • Now: Introduces type safety for LLM calls.
  • Required updates to ensure type checking.
  • Simplification of data handling.

Checklist

Before submitting a pull request, please ensure that you mark these task.

  • Ran fastapi run --reload app/main.py or docker compose up in the repository root and test.
  • If you've fixed a bug or added code that is tested and has test cases.

Notes

Please add here if any other information is required for the reviewer.

Original PR description

Issue

Closes #PLEASE_TYPE_ISSUE_NUMBER

Summary

Explain the motivation for making this change. What existing problem does the pull request solve?

Checklist

Before submitting a pull request, please ensure that you mark these task.

  • Ran fastapi run --reload app/main.py or docker compose up in the repository root and test.
  • If you've fixed a bug or added code that is tested and has test cases.

Notes

Please add here if any other information is required for the reviewer.

Summary by CodeRabbit

  • New Features

    • Added strongly typed configuration for text, speech-to-text, and text-to-speech models.
    • Improved speech-to-speech language validation and standardized language formatting.
    • Audio responses now provide signed URLs for playback.
  • Bug Fixes

    • Improved compatibility with varied configuration formats across model providers.
    • Preserved explicitly configured temperature values, including zero.
    • Improved tracing and evaluation reliability for typed model settings.

@Prajna1999 Prajna1999 self-assigned this Jul 28, 2026
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0164041b-953d-47d0-9766-0ecf83eef8e8

📥 Commits

Reviewing files that changed from the base of the PR and between 934052b and 438a894.

📒 Files selected for processing (1)
  • backend/app/tests/services/llm/test_sts.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/app/tests/services/llm/test_sts.py

📝 Walkthrough

Walkthrough

The PR introduces typed Kaapi completion variants, centralizes parameter normalization, moves STS language validation into request models, updates execution paths and tests, and centralizes persisted LLM audio output resolution.

Changes

LLM configuration and execution

Layer / File(s) Summary
Typed completion and STS contracts
backend/app/models/llm/*
Kaapi text, STT, and TTS configurations now use typed variants and a shared builder. STS language fields use a constrained BCP-47 literal type with casing normalization and output-language validation.
Parameter normalization and provider execution
backend/app/services/llm/*, backend/app/crud/*, backend/app/services/evaluations/*, backend/app/tests/...
Typed and dictionary parameter shapes are normalized for provider mapping, batch execution, tracing, validation, proxy handling, and Kaapi job execution. Related tests use the new builder and typed attribute access.
Speech-to-speech route validation
backend/app/api/routes/llm_sts.py, backend/app/services/llm/chain/utils.py, backend/app/tests/services/llm/test_sts.py
The STS route delegates language validation to SpeechToSpeechRequest, builds typed completion configs, and derives supported language codes from STSLanguageCode.
Persisted LLM output resolution
backend/app/api/routes/llm.py
LLM call status responses validate persisted output payloads and presign audio URI references before returning them.
Typed parameter behavior tests
backend/app/tests/models/llm/test_request.py
Tests verify typed parameter construction and preserve explicit zero temperature while omitting unspecified temperature defaults.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: kartpop, akhileshnegi, vprashrex

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.59% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: adding type-safety improvements across LLM configs and routes.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/type-safety-llm-calls

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.

@github-actions github-actions Bot changed the title genesis commit, yolo feat(llm): Introduce type safety checks Jul 28, 2026
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown

OpenAPI changes   🔴 8 breaking changes

Caution

Downstream consumers may need an update before merging.

Breaking changes  ·  8
Method Path Change
🔴 POST /api/v1/configs added the new required request property config_blob/completion/anyOf[subschema #3: ProxyCompletionConfig -> subschema #5: ProxyCompletionConfig]/params/client_llm_url
🔴 POST /api/v1/configs removed #/components/schemas/KaapiCompletionConfig from the config_blob/completion request property anyOf list
🔴 POST /api/v1/llm/call added the new required request property config/blob/anyOf[subschema #1: ConfigBlob]/completion/anyOf[subschema #3: ProxyCompletionConfig -> subschema #5: ProxyCompletionConfig]/params/client_llm_url
🔴 POST /api/v1/llm/call removed #/components/schemas/KaapiCompletionConfig from the config/blob/anyOf[subschema #1: ConfigBlob]/completion request property anyOf list
🔴 POST /api/v1/llm/chain added the new required request property blocks/items/config/blob/anyOf[subschema #1: ConfigBlob]/completion/anyOf[subschema #3: ProxyCompletionConfig -> subschema #5: ProxyCompletionConfig]/params/client_llm_url
🔴 POST /api/v1/llm/chain removed #/components/schemas/KaapiCompletionConfig from the blocks/items/config/blob/anyOf[subschema #1: ConfigBlob]/completion request property anyOf list
🔴 POST /api/v1/llm/chain/sts request property input_language was restricted to a list of enum values
🔴 POST /api/v1/llm/chain/sts request property output_language/anyOf[subschema #1]/ was restricted to a list of enum values
Full changelog  ·  62
Method Path Change
🔴 POST /api/v1/configs added the new required request property config_blob/completion/anyOf[subschema #3: ProxyCompletionConfig -> subschema #5: ProxyCompletionConfig]/params/client_llm_url
🔴 POST /api/v1/configs removed #/components/schemas/KaapiCompletionConfig from the config_blob/completion request property anyOf list
🔴 POST /api/v1/llm/call added the new required request property config/blob/anyOf[subschema #1: ConfigBlob]/completion/anyOf[subschema #3: ProxyCompletionConfig -> subschema #5: ProxyCompletionConfig]/params/client_llm_url
🔴 POST /api/v1/llm/call removed #/components/schemas/KaapiCompletionConfig from the config/blob/anyOf[subschema #1: ConfigBlob]/completion request property anyOf list
🔴 POST /api/v1/llm/chain added the new required request property blocks/items/config/blob/anyOf[subschema #1: ConfigBlob]/completion/anyOf[subschema #3: ProxyCompletionConfig -> subschema #5: ProxyCompletionConfig]/params/client_llm_url
🔴 POST /api/v1/llm/chain removed #/components/schemas/KaapiCompletionConfig from the blocks/items/config/blob/anyOf[subschema #1: ConfigBlob]/completion request property anyOf list
🔴 POST /api/v1/llm/chain/sts request property input_language was restricted to a list of enum values
🔴 POST /api/v1/llm/chain/sts request property output_language/anyOf[subschema #1]/ was restricted to a list of enum values
🟢 removed the schema KaapiCompletionConfig
🟢 POST /api/v1/configs added #/components/schemas/KaapiTextCompletionConfig, #/components/schemas/KaapiSTTCompletionConfig, #/components/schemas/KaapiTTSCompletionConfig to the config_blob/completion request property anyOf list
🟢 POST /api/v1/llm/call added #/components/schemas/KaapiTextCompletionConfig, #/components/schemas/KaapiSTTCompletionConfig, #/components/schemas/KaapiTTSCompletionConfig to the config/blob/anyOf[subschema #1: ConfigBlob]/completion request property anyOf list
🟢 POST /api/v1/llm/chain added #/components/schemas/KaapiTextCompletionConfig, #/components/schemas/KaapiSTTCompletionConfig, #/components/schemas/KaapiTTSCompletionConfig to the blocks/items/config/blob/anyOf[subschema #1: ConfigBlob]/completion request property anyOf list
🟢 POST /api/v1/llm/chain/sts added the new as-IN enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new as-IN enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new auto enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new auto enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new bn-IN enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new bn-IN enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new brx-IN enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new brx-IN enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new doi-IN enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new doi-IN enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new en-IN enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new en-IN enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new gu-IN enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new gu-IN enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new hi-IN enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new hi-IN enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new kn-IN enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new kn-IN enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new kok-IN enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new kok-IN enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new ks-IN enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new ks-IN enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new mai-IN enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new mai-IN enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new ml-IN enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new ml-IN enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new mni-IN enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new mni-IN enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new mr-IN enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new mr-IN enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new ne-IN enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new ne-IN enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new od-IN enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new od-IN enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new pa-IN enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new pa-IN enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new sa-IN enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new sa-IN enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new sat-IN enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new sat-IN enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new sd-IN enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new sd-IN enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new ta-IN enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new ta-IN enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new te-IN enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new te-IN enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new unknown enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new unknown enum value to the request property output_language/anyOf[subschema #1]/
🟢 POST /api/v1/llm/chain/sts added the new ur-IN enum value to the request property input_language
🟢 POST /api/v1/llm/chain/sts added the new ur-IN enum value to the request property output_language/anyOf[subschema #1]/

main0d46ee82 · generated by oasdiff

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (1)
backend/app/tests/models/llm/test_request.py (1)

10-17: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep build_kaapi_completion_config call sites aligned with the typed contract.

The builder signature accepts Provider values, CompletionType, and typed params models, but these tests pass raw strings and plain dictionaries. Update the affected tests to use Provider.OPENAI/Provider.GOOGLE, CompletionType.TEXT, and TextLLMParams, or narrow the builder contract if raw dicts are intentional.

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

In `@backend/app/tests/models/llm/test_request.py` around lines 10 - 17, Update
build_kaapi_completion_config call sites to satisfy its typed contract: in
backend/app/tests/models/llm/test_request.py lines 10-17 and
backend/app/tests/services/llm/test_mappers.py lines 895-899, use
Provider.OPENAI or Provider.GOOGLE, CompletionType.TEXT, and TextLLMParams
instead of raw strings and dictionaries; preserve the existing test scenarios
without narrowing the builder contract.

Source: Coding guidelines

🧹 Nitpick comments (5)
backend/app/api/routes/llm.py (1)

35-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move output resolution to an LLM service.

This route now owns storage access, URI conversion, and model validation. Extract _resolve_llm_output into the LLM service layer; keep the route limited to request/response orchestration. As per coding guidelines, API routes expose REST endpoints while services contain business logic.

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

In `@backend/app/api/routes/llm.py` around lines 35 - 65, Move the
_resolve_llm_output function and its storage, URI conversion, presigned-URL
error handling, and LLMOutput validation logic into the LLM service layer.
Update the route to call the service-level resolver, leaving the endpoint
responsible only for request/response orchestration and removing its direct
business-logic dependencies.

Source: Coding guidelines

backend/app/models/llm/request.py (3)

345-347: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use X | Y instead of Union[...] inside Annotated[...].

Static analysis flags both discriminated-union type aliases for the deprecated Union[...] syntax; the codebase already targets Python 3.11+, so the PEP 604 | syntax works fine here (including inside Annotated).

♻️ Suggested fix
 KaapiCompletionConfig = Annotated[
-    Union[
-        KaapiTextCompletionConfig, KaapiSTTCompletionConfig, KaapiTTSCompletionConfig
-    ],
+    KaapiTextCompletionConfig | KaapiSTTCompletionConfig | KaapiTTSCompletionConfig,
     Field(discriminator="type"),
 ]
...
 CompletionConfig = Annotated[
-    Union[
-        NativeCompletionConfig,
-        KaapiTextCompletionConfig,
-        KaapiSTTCompletionConfig,
-        KaapiTTSCompletionConfig,
-        ProxyCompletionConfig,
-    ],
+    NativeCompletionConfig
+    | KaapiTextCompletionConfig
+    | KaapiSTTCompletionConfig
+    | KaapiTTSCompletionConfig
+    | ProxyCompletionConfig,
     Field(discriminator="provider"),
 ]

Also applies to: 397-403

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

In `@backend/app/models/llm/request.py` around lines 345 - 347, Replace the
Union[...] syntax in both discriminated-union type aliases around the Kaapi
completion configurations with PEP 604 | syntax, preserving the existing
Annotated metadata and member types.

Source: Linters/SAST tools


308-320: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate _default_provider validator between STT and TTS variants.

The identical model_validator(mode="after") defaulting provider to Provider.GOOGLE is copy-pasted between KaapiSTTCompletionConfig and KaapiTTSCompletionConfig. Since KaapiTextCompletionConfig intentionally omits this default, it can't simply move to _KaapiCompletionConfigBase, but it could be extracted into a small mixin/shared helper that only STT/TTS opt into, to avoid future drift between the two copies.

♻️ Example extraction
+class _DefaultsToGoogleMixin:
+    `@model_validator`(mode="after")
+    def _default_provider(self) -> Self:
+        if self.provider is None:
+            self.provider = Provider.GOOGLE
+        return self
+
+
-class KaapiSTTCompletionConfig(_KaapiCompletionConfigBase):
+class KaapiSTTCompletionConfig(_KaapiCompletionConfigBase, _DefaultsToGoogleMixin):
     type: Literal[CompletionType.STT] = Field(
         ..., description="Completion config type. Params schema varies by type"
     )
     params: STTLLMParams = Field(
         ..., description="Kaapi-standardized parameters mapped to provider-specific API"
     )
-
-    `@model_validator`(mode="after")
-    def _default_provider(self) -> Self:
-        if self.provider is None:
-            self.provider = Provider.GOOGLE
-        return self

Also applies to: 323-335

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

In `@backend/app/models/llm/request.py` around lines 308 - 320, Extract the
duplicated _default_provider model validator from KaapiSTTCompletionConfig and
KaapiTTSCompletionConfig into a shared mixin or helper, then have only those STT
and TTS configuration classes opt into it. Preserve KaapiTextCompletionConfig’s
behavior of leaving provider unset and keep the existing Provider.GOOGLE default
behavior unchanged.

1074-1096: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated ("auto", "unknown") sentinel tuple into a named constant.

The same magic tuple appears in both normalize_language_casing and validate_output_language. As per coding guidelines, "Do not use magic values; extract repeated literals into constants, enums, or settings." A shared module-level constant (e.g. STT_ONLY_LANGUAGE_SENTINELS = ("auto", "unknown")) would remove the duplication and keep the two validators in sync if a sentinel is ever added/removed.

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

In `@backend/app/models/llm/request.py` around lines 1074 - 1096, Define a shared
module-level constant for the STT-only language sentinels ("auto", "unknown"),
then update normalize_language_casing and validate_output_language to reference
it instead of repeating the tuple. Preserve the existing sentinel handling and
validation behavior.

Source: Coding guidelines

backend/app/api/routes/llm_sts.py (1)

109-122: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Narrow provider type hint to match the callee's contract.

_inline_call_config's provider: str | None is wider than build_kaapi_completion_config's provider: KaapiProvider | None. Since this is an internal typed-safety refactor, tightening the hint here would keep the type-checking benefit end-to-end.

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

In `@backend/app/api/routes/llm_sts.py` around lines 109 - 122, The
_inline_call_config provider parameter is broader than
build_kaapi_completion_config accepts. Narrow its type annotation from str |
None to KaapiProvider | None, using the existing KaapiProvider symbol and
preserving the provider forwarding behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/app/api/routes/llm.py`:
- Around line 59-61: Update the logger.warning call in _resolve_llm_output so
its prefix uses _resolve_llm_output instead of get_llm_call_status, while
preserving the existing error details and job_id context.
- Line 57: Extract the 3600-second expiration value used in the
storage.get_signed_url call within the surrounding route logic into a clearly
named, unit-suffixed constant, then pass that constant as expires_in. Keep the
existing one-hour TTL behavior unchanged.
- Around line 35-40: Update _resolve_llm_output to use the narrowest concrete
type for raw_content based on the validated LLM response schema, replacing
unconstrained dict, and change its return annotation from LLMOutput | None to
LLMOutput. Preserve the existing validation behavior where valid input returns
LLMOutput and invalid input raises.

In `@backend/app/crud/assessment/batch.py`:
- Around line 417-423: Update the typed-parameter normalization in the batch
completion flow to call model_dump with exclude_unset=True alongside
exclude_none=True, preserving only explicitly provided Kaapi values such as
temperature. Ensure the nearby Temperature suppression checks in the same
normalization logic are based on provided values, not default-populated fields,
while leaving native dict handling unchanged.

In `@backend/app/models/llm/constants.py`:
- Around line 66-68: Update the comments above STSLanguageCode to identify this
Literal alias as the single source of truth for accepted speech-to-speech
language codes, and state that SUPPORTED_LANGUAGE_CODES is derived from it via
get_args. Do not describe SUPPORTED_LANGUAGE_CODES as authoritative.

In `@backend/app/services/llm/jobs.py`:
- Around line 837-847: Update the guardrail-direct-response path before the
branch around config_blob.completion.params.get("model") to support typed Kaapi
text, STT, and TTS configurations. Normalize those configs with
transform_kaapi_config_to_native or convert params through kaapi_params_as_dict
before accessing model, while preserving the existing guarded-response behavior.

---

Outside diff comments:
In `@backend/app/tests/models/llm/test_request.py`:
- Around line 10-17: Update build_kaapi_completion_config call sites to satisfy
its typed contract: in backend/app/tests/models/llm/test_request.py lines 10-17
and backend/app/tests/services/llm/test_mappers.py lines 895-899, use
Provider.OPENAI or Provider.GOOGLE, CompletionType.TEXT, and TextLLMParams
instead of raw strings and dictionaries; preserve the existing test scenarios
without narrowing the builder contract.

---

Nitpick comments:
In `@backend/app/api/routes/llm_sts.py`:
- Around line 109-122: The _inline_call_config provider parameter is broader
than build_kaapi_completion_config accepts. Narrow its type annotation from str
| None to KaapiProvider | None, using the existing KaapiProvider symbol and
preserving the provider forwarding behavior.

In `@backend/app/api/routes/llm.py`:
- Around line 35-65: Move the _resolve_llm_output function and its storage, URI
conversion, presigned-URL error handling, and LLMOutput validation logic into
the LLM service layer. Update the route to call the service-level resolver,
leaving the endpoint responsible only for request/response orchestration and
removing its direct business-logic dependencies.

In `@backend/app/models/llm/request.py`:
- Around line 345-347: Replace the Union[...] syntax in both discriminated-union
type aliases around the Kaapi completion configurations with PEP 604 | syntax,
preserving the existing Annotated metadata and member types.
- Around line 308-320: Extract the duplicated _default_provider model validator
from KaapiSTTCompletionConfig and KaapiTTSCompletionConfig into a shared mixin
or helper, then have only those STT and TTS configuration classes opt into it.
Preserve KaapiTextCompletionConfig’s behavior of leaving provider unset and keep
the existing Provider.GOOGLE default behavior unchanged.
- Around line 1074-1096: Define a shared module-level constant for the STT-only
language sentinels ("auto", "unknown"), then update normalize_language_casing
and validate_output_language to reference it instead of repeating the tuple.
Preserve the existing sentinel handling and validation behavior.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f7a0b7f7-4cae-4c8a-bb6c-1c2830b7950f

📥 Commits

Reviewing files that changed from the base of the PR and between e0c64fc and 934052b.

📒 Files selected for processing (26)
  • backend/app/api/routes/llm.py
  • backend/app/api/routes/llm_sts.py
  • backend/app/core/langfuse/langfuse.py
  • backend/app/crud/assessment/batch.py
  • backend/app/crud/evaluations/core.py
  • backend/app/crud/model_config.py
  • backend/app/models/llm/__init__.py
  • backend/app/models/llm/constants.py
  • backend/app/models/llm/request.py
  • backend/app/services/evaluations/batch_job.py
  • backend/app/services/llm/chain/utils.py
  • backend/app/services/llm/jobs.py
  • backend/app/services/llm/mappers.py
  • backend/app/tests/api/routes/configs/test_version.py
  • backend/app/tests/api/routes/test_evaluation_fast.py
  • backend/app/tests/api/routes/test_evaluation_v2.py
  • backend/app/tests/api/routes/test_improve_prompt.py
  • backend/app/tests/api/routes/test_llm.py
  • backend/app/tests/crud/evaluations/test_fast_judge.py
  • backend/app/tests/crud/test_llm.py
  • backend/app/tests/models/llm/test_request.py
  • backend/app/tests/services/llm/test_jobs.py
  • backend/app/tests/services/llm/test_mappers.py
  • backend/app/tests/services/llm/test_sts.py
  • backend/app/tests/utils/llm.py
  • backend/app/tests/utils/test_data.py

Comment on lines +35 to +40
def _resolve_llm_output(
raw_content: dict,
project_id: int,
session: Session,
job_id: UUID,
) -> LLMOutput | None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Narrow the helper contract.

dict is unconstrained, and this function never returns None: validation either returns LLMOutput or raises. As per coding guidelines, every parameter and return value needs a narrow type.

Proposed fix
 def _resolve_llm_output(
-    raw_content: dict,
+    raw_content: dict[str, object],
     project_id: int,
     session: Session,
     job_id: UUID,
-) -> LLMOutput | None:
+) -> LLMOutput:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _resolve_llm_output(
raw_content: dict,
project_id: int,
session: Session,
job_id: UUID,
) -> LLMOutput | None:
def _resolve_llm_output(
raw_content: dict[str, object],
project_id: int,
session: Session,
job_id: UUID,
) -> LLMOutput:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/api/routes/llm.py` around lines 35 - 40, Update
_resolve_llm_output to use the narrowest concrete type for raw_content based on
the validated LLM response schema, replacing unconstrained dict, and change its
return annotation from LLMOutput | None to LLMOutput. Preserve the existing
validation behavior where valid input returns LLMOutput and invalid input
raises.

Source: Coding guidelines

s3_path = inner.get("value", "")
try:
storage = get_cloud_storage(session, project_id)
inner["value"] = storage.get_signed_url(s3_path, expires_in=3600)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Name the presigned-URL TTL.

3600 is an undocumented policy value. Extract it into a clearly unit-suffixed constant. As per coding guidelines, do not use magic values.

Proposed fix
 _LLM_OUTPUT_ADAPTER: TypeAdapter[LLMOutput] = TypeAdapter(LLMOutput)
+PRESIGNED_AUDIO_URL_TTL_SECONDS = 3_600

-            inner["value"] = storage.get_signed_url(s3_path, expires_in=3600)
+            inner["value"] = storage.get_signed_url(
+                s3_path, expires_in=PRESIGNED_AUDIO_URL_TTL_SECONDS
+            )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
inner["value"] = storage.get_signed_url(s3_path, expires_in=3600)
_PRESIGNED_AUDIO_URL_TTL_SECONDS = 3_600
inner["value"] = storage.get_signed_url(
s3_path, expires_in=PRESIGNED_AUDIO_URL_TTL_SECONDS
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/api/routes/llm.py` at line 57, Extract the 3600-second expiration
value used in the storage.get_signed_url call within the surrounding route logic
into a clearly named, unit-suffixed constant, then pass that constant as
expires_in. Keep the existing one-hour TTL behavior unchanged.

Source: Coding guidelines

Comment on lines +59 to +61
logger.warning(
f"[get_llm_call_status] Failed to generate presigned URL for audio: {e} | job_id={job_id}"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the helper’s name in this log prefix.

This warning originates in _resolve_llm_output, not get_llm_call_status; the current prefix misattributes presigning failures. As per coding guidelines, every log line must be prefixed with its function name.

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

In `@backend/app/api/routes/llm.py` around lines 59 - 61, Update the
logger.warning call in _resolve_llm_output so its prefix uses
_resolve_llm_output instead of get_llm_call_status, while preserving the
existing error details and job_id context.

Source: Coding guidelines

Comment on lines +417 to +423
# Native params are a plain dict; Kaapi params are now a typed submodel.
raw_params = completion.params
params = (
dict(raw_params)
if isinstance(raw_params, dict)
else raw_params.model_dump(exclude_none=True)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate file =="
git ls-files | rg '(^|/)backend/app/crud/assessment/batch\.py$|TextLLMParams|completion\.params|model_dump' || true

echo
echo "== Relevant snippets =="
if [ -f backend/app/crud/assessment/batch.py ]; then
  nl -ba backend/app/crud/assessment/batch.py | sed -n '380,450p'
fi

echo
echo "== Search TextLLMParams definition/usages =="
rg -n "class TextLLMParams|TextLLMParams|model_dump\\(|params\\.model_dump" backend/app -S || true

echo
echo "== Pydantic/model validation =="
rg -n "from pydantic import|import pydantic|BaseModel|Field\\(" backend/app -S || true

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 290


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== File exists and line range =="
head -n 450 backend/app/crud/assessment/batch.py | tail -n 80

echo
echo "== Locate TextLLMParams definition/usages =="
grep -RIn "class TextLLMParams\|TextLLMParams\|model_dump\|completion\.params" backend/app -S || true

echo
echo "== Pydantic deps/framework snippets =="
grep -RIn "from pydantic import\|import pydantic\|BaseModel\|Field(" backend/app pyproject.toml requirements*.txt poetry.lock -S 2>/dev/null || true

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 3254


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== TextLLMParams definition/usages =="
grep -RIn "class TextLLMParams\|TextLLMParams\|model_dump\|completion\.params" backend/app || true

echo
echo "== Pydantic deps/framework snippets =="
grep -RIn "from pydantic import\|import pydantic\|BaseModel\|Field(" backend/app pyproject.toml requirements*.txt poetry.lock 2>/dev/null || true

echo
echo "== Behavioral probe for Pydantic exclude_none vs exclude_unset =="
python3 - <<'PY'
import sys
print("python", sys.version)
try:
    import pydantic
    from pydantic import BaseModel, Field
    print("pydantic", pydantic.__version__)
except Exception as e:
    print("pydantic unavailable:", type(e).__name__, str(e))
    sys.exit(0)

class Params(BaseModel):
    temperature: float = 0.1
    top_p: float | None = None

p = Params()
print("exclude_none:", p.model_dump(exclude_none=True))
print("exclude_unset:", p.model_dump(exclude_unset=True))
print("both:", p.model_dump(exclude_none=True, exclude_unset=True))
PY

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 50387


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== TextLLMParams definition =="
sed -n '1,90p' backend/app/models/llm/request.py

echo
echo "== Assessment model params default =="
sed -n '300,340p' backend/app/models/assessment.py

echo
echo "== Mapper behavior for unset temperature =="
sed -n '1,90p' backend/app/services/llm/mappers.py
sed -n '1,180p' backend/app/tests/services/llm/test_mappers.py

echo
echo "== Pydantic behavioral probe =="
python3 - <<'PY'
import sys
print("python", sys.version)
try:
    import pydantic
    from pydantic import BaseModel, Field
    print("pydantic", pydantic.__version__)
except Exception as e:
    print("pydantic unavailable:", type(e).__name__, str(e))
    sys.exit(0)

class Params(BaseModel):
    temperature: float = 0.1
    top_p: float | None = None

p = Params()
print("exclude_none:", p.model_dump(exclude_none=True))
print("exclude_unset:", p.model_dump(exclude_unset=True))
print("both:", p.model_dump(exclude_none=True, exclude_unset=True))
PY

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 13820


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pydantic import BaseModel, Field

class Params(BaseModel):
    temperature: float | None = 0.1
    top_p: float | None = None

p = Params()
print("exclude_none:", p.model_dump(exclude_none=True))
print("exclude_unset:", p.model_dump(exclude_unset=True))
print("both:", p.model_dump(exclude_none=True, exclude_unset=True))
PY

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 290


🌐 Web query:

Pydantic V2 model_dump exclude_none exclude_unset defaults unset fields

💡 Result:

In Pydantic V2, the model_dump method (and model_dump_json) provides specific parameters to control which fields are included in the serialized output. These parameters are essential for tasks like partial updates (PATCH requests) where you only want to send fields that the user explicitly provided [1][2]. Key Parameters for Field Exclusion: exclude_unset: When set to True, this excludes any fields that were not explicitly set during model instantiation [1][3]. Pydantic tracks which fields were provided using the model_fields_set property [1][4]. If you instantiate a model and only provide one field, only that field will appear in the output when exclude_unset=True [1]. Note that if you modify a field on the instance after it has been created, it is considered set and will be included [1][4]. exclude_none: When set to True, this excludes any fields whose value is None, regardless of whether they were explicitly set or assigned a default [1][3]. This is useful for cleaning up outputs by removing null values [5]. exclude_defaults: When set to True, this excludes any fields whose value is equal to their default value (using the == equality operator) [1][4]. Distinction: exclude_unset focuses on the origin of the value (was it provided at creation?) [1]. exclude_none focuses on the actual value itself (is it None?) [1]. exclude_defaults focuses on whether the current value matches the defined default value [1]. These parameters can be used independently or in combination to precisely tailor the serialization output [1][2]. If a field is explicitly set to None, exclude_unset=True will keep it in the output, while exclude_none=True will remove it [5][6]. If you need to detect if a field was missing versus set to None, you can inspect the model_fields_set attribute directly [6].

Citations:


Preserve unset Kaapi params during batch normalization.

TextLLMParams.temperature defaults to 0.1, so exclude_none=True serializes it as an explicit temperature when the typed config omits it. That normalized temperature=0.1 then overrides provider/model defaults, unlike native dict params. Add exclude_unset=True here, and keep Temperature suppression checks keyed to provided values rather than fields with defaults.

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

In `@backend/app/crud/assessment/batch.py` around lines 417 - 423, Update the
typed-parameter normalization in the batch completion flow to call model_dump
with exclude_unset=True alongside exclude_none=True, preserving only explicitly
provided Kaapi values such as temperature. Ensure the nearby Temperature
suppression checks in the same normalization logic are based on provided values,
not default-populated fields, while leaving native dict handling unchanged.

Comment on lines +66 to +68
# BCP-47 language codes accepted by the speech-to-speech endpoint (STT input /
# TTS output). Single source of truth: `SUPPORTED_LANGUAGE_CODES` in
# `app/services/llm/chain/utils.py` derives from this via `get_args`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Make the alias the documented source of truth.

This comment calls SUPPORTED_LANGUAGE_CODES authoritative while also stating that it is derived from STSLanguageCode. Since utils.py derives the set via get_args, describe this Literal as the source of truth to avoid future edits to the wrong declaration.

Suggested wording
-# TTS output). Single source of truth: `SUPPORTED_LANGUAGE_CODES` in
-# `app/services/llm/chain/utils.py` derives from this via `get_args`.
+# TTS output). This Literal is the single source of truth; `SUPPORTED_LANGUAGE_CODES`
+# in `app/services/llm/chain/utils.py` is derived from it via `get_args`.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# BCP-47 language codes accepted by the speech-to-speech endpoint (STT input /
# TTS output). Single source of truth: `SUPPORTED_LANGUAGE_CODES` in
# `app/services/llm/chain/utils.py` derives from this via `get_args`.
# BCP-47 language codes accepted by the speech-to-speech endpoint (STT input /
# TTS output). This Literal is the single source of truth; `SUPPORTED_LANGUAGE_CODES`
# in `app/services/llm/chain/utils.py` is derived from it via `get_args`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/models/llm/constants.py` around lines 66 - 68, Update the
comments above STSLanguageCode to identify this Literal alias as the single
source of truth for accepted speech-to-speech language codes, and state that
SUPPORTED_LANGUAGE_CODES is derived from it via get_args. Do not describe
SUPPORTED_LANGUAGE_CODES as authoritative.

Comment on lines +837 to 847
if isinstance(
completion_config,
(
KaapiTextCompletionConfig,
KaapiSTTCompletionConfig,
KaapiTTSCompletionConfig,
),
):
completion_config, warnings = transform_kaapi_config_to_native(
session=session, kaapi_config=completion_config
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Normalize typed parameters before the guardrail short-circuit.

The new Kaapi configs store params as typed models, but the earlier guardrail-direct-response branch still calls config_blob.completion.params.get("model") at Line 585 before this conversion. A guardrail hit with a typed config can therefore raise AttributeError instead of returning the guarded response. Normalize parameters before that branch or use kaapi_params_as_dict there.

Based on the supplied typed config definitions, params is a typed model for the Kaapi text/STT/TTS variants.

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

In `@backend/app/services/llm/jobs.py` around lines 837 - 847, Update the
guardrail-direct-response path before the branch around
config_blob.completion.params.get("model") to support typed Kaapi text, STT, and
TTS configurations. Normalize those configs with
transform_kaapi_config_to_native or convert params through kaapi_params_as_dict
before accessing model, while preserving the existing guarded-response behavior.

@Prajna1999 Prajna1999 added the breaking-change-approved Reviewer-acknowledged API breaking change label Jul 28, 2026
@Prajna1999
Prajna1999 marked this pull request as ready for review July 28, 2026 10:20
@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.02632% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
backend/app/services/llm/mappers.py 81.81% 2 Missing ⚠️
backend/app/models/llm/request.py 97.61% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@Prajna1999 Prajna1999 added ready-for-review and removed breaking-change-approved Reviewer-acknowledged API breaking change labels Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant