Commit 975235b
authored
feat: flag deprecated models and redesign admin models table (eneo-ai#315)
* feat: flag deprecated models in admin UI using LiteLLM deprecation data
Enrich API responses with deprecation_date from litellm.model_cost at
serialization time. Models past their deprecation date are automatically
flagged as deprecated. The frontend shows red/yellow labels in the
Details column and exposes a Migrate action directly in the dropdown
menu for deprecated completion models.
Also adds SDK functions for migration history endpoints (already existed
in backend but were never wired to frontend) and a Migration History
tab in the admin models page.
Backend:
- New deprecation_lookup utility with get_litellm_deprecation_date()
- deprecation_date field on CompletionModelPublic, EmbeddingModelPublic,
TranscriptionModelPublic (computed at serialization, no DB migration)
- 21 new unit tests covering lookup and enrichment logic
Frontend:
- Red "Deprecated" / yellow "Retiring YYYY-MM-DD" labels in ModelLabels
- Migrate button in ModelActions dropdown for deprecated models
- getMigrationHistory/getAllMigrationHistory SDK functions
- MigrationHistoryPanel component + tab in admin/models page
- Translation keys for EN and SV
* feat: redesign admin models table for clarity and accessibility
Replace the dense tag-based table with a cleaner design:
- Remove Details column tags, replace with compact status icons
(ModelStatusIcons: deprecation, reasoning, vision, tools)
- Remove Security column (moved to ModelDetailDialog)
- Add status indicator dot on each model name (green/red/yellow/gray)
- New ModelDetailDialog: click model name to see full details
(capabilities, hosting, security, metadata, deprecation alert)
- Deprecation warning banner above table with model count
- Row tinting for deprecated/retiring models (CSS :has selector)
- Reorder dropdown actions: neutral first, destructive last
- Migrate action available for all completion models, not just deprecated
- Enable Vite polling for HMR in Docker dev environment
* feat: improve model migration with validation, security checks, and history
Migration validation:
- New GET /completion-models/{id}/migration-validate endpoint for preflight
- Server-side compatibility check on target selection (no client-side approx)
- Security classification blocker: prevents migration to lower-classified model
when spaces require higher classification (cannot be overridden)
- Race condition fix: stale validation responses discarded
- Dual warning format: human-readable warnings + machine-readable warning_codes
Migration execution:
- Fix kwargs reset being a blocker (now informational only)
- Remove auto-delete that ran even on failure; delete only after success
- Spaces included in default migration via MIGRATABLE_ENTITY_TYPES
- Audit logging with COMPLETION_MODEL_MIGRATED action type
Migration history:
- Alembic migration: FK ondelete CASCADE → SET NULL, add model name columns
- Store from_model_name/to_model_name at creation (survives model deletion)
- ModelMigrationHistory: model IDs now Optional[UUID]
- Expandable detail rows: breakdown per entity type, warnings, errors, duration
- Auto-refresh via migrationHistoryRefreshVersion store
Migration dialog UX:
- Impact preview: shows affected resources grouped by type with details
- Collapsible sections per entity type (assistants, apps, questions)
- Spaces info: "Target model will be enabled on N spaces"
- Preflight warnings shown on target selection with translated messages
- Security blocker shown as red panel, disables migrate button
- SDK: validateMigration, getUsageStats, getUsageDetails functions
* test: add migration validation, history, and endpoint tests
48 new unit tests across 3 files:
- test_migration_validation.py (22): compatibility checks, security blockers,
warning codes, kwargs reset, multiple warnings combined
- test_migration_history.py (15): stored model names, SET NULL handling,
Pydantic nullable IDs, fallback logic, serialization roundtrip
- test_migration_endpoint.py (11): audit action type, category mapping,
metadata, ValidationResult/MigrationRequest schema tests
Updated existing tests for warning_codes field and audit action registration.
* fix: security blocker bypass and impact total underreporting
- Fix security classification blocker check: was searching for codes in
warnings (human-readable text) instead of warning_codes. With
confirm_migration=true, a security-blocked migration could pass through.
- Fix impact preview total: usage/details endpoint now counts real total
across all entity types via separate COUNT queries, instead of returning
len(items) which was just the current page size.
* fix: add type annotations to deprecation_lookup for pyright
* fix: set model name columns after init to satisfy pyright
* docs: inlang
* feat: consolidate model settings into detail and edit dialogs
Move scattered model settings into a unified flow:
- ModelDetailDialog is now read-only info view
- EditModelDialog handles all editing: parameters, security
classification, default model status
- ModelActions dropdown simplified to Edit, Migrate, Delete
- Table status dots: red=disabled, icons for deprecated/retiring
- SelectSecurityClassification trigger now full-width
* feat: add security classification to model creation wizard
Security classification can now be set when adding a model, so admins
don't need to create and then immediately edit to set classification.
Uses a separate API call after create since the backend create endpoint
doesn't accept security_classification directly.
* refactor: clean up model name display in table and detail view
Table name column now shows only the display name — model identifier
and context window moved to the detail dialog. Detail dialog always
shows both display name and model identifier with consistent styling.
* feat: preserve historical question attribution during model migration
Questions are historical records that must not be migrated — doing so
falsely attributes answers to the target model and corrupts token usage
analytics.
Changes:
- Exclude questions from MIGRATABLE_ENTITY_TYPES
- Add migrated_to_model_id (FK RESTRICT) and deleted_at to completion_models
- Change questions FK from SET NULL to RESTRICT as safety net
- Mark source model with migrated_to_model_id after successful migration
- Block re-migration of already-migrated models (both preflight and execute)
- Soft-delete replaces hard-delete in both tenant and sysadmin endpoints
- Update can_access in all three paths (domain, AIModelsService, pydantic)
- Filter migrated/deleted models from space assembler, model selection UIs
- Add MODEL_IN_USE check before soft-delete via shared has_active_references
- Frontend: remove auto-delete after migration, add Migrated label, lock
enable switch, exclude migrated from target lists and space settings
* feat: add weekly lifecycle cleanup for orphaned completion models
Automatically hard-deletes completion models once all references are gone
(questions gallrad, active entities migrated, no migration pointers).
- Weekly ARQ cron job (Sunday 4 AM) targeting soft-deleted and migrated models
- Reuses shared has_active_references from CompletionModelsRepository
- Reconciles soft-deleted template FK references before hard-delete
- IntegrityError classified as db_restrict skip, not unexpected error
- Preserves migration history via immutable from/to_model_original_id columns
- History service falls back to original_id when FK is SET NULL after cleanup
- Indexes on deleted_at, migrated_to_model_id, and original_id columns
* refactor: replace original_id columns with provider_type snapshots in migration history
Simplify the migration history model: remove from/to_model_original_id
(dead UUIDs pointing to deleted rows) and replace with provider_type
snapshots that give readable audit context.
- Remove from_model_original_id and to_model_original_id columns
- Add from_provider_type and to_provider_type as audit snapshots
- Backfill provider_type from model_providers for existing records
- Simplify repo queries to only match live FK IDs (no OR fallback)
- History service returns None for model IDs after cleanup (not stale UUIDs)
- Per-model history works for live models; use tenant/migration_id after cleanup
* fix: unblock lifecycle cleanup typecheck and unit tests
* test: align sysadmin completion model delete with soft-delete semantics
* fix: add explicit type annotations to Dropdown ctx for d.ts generation
Fixes svelte-package failing to generate type declarations due to
inferred types referencing internal @melt-ui/svelte modules.
* fix: restore migration_id column and post-rebase corrections
- migration_id column was lost during conflict resolution
- audit user_action count updated from 29 to 30 to reflect
the COMPLETION_MODEL_MIGRATED action added by the
deprecation-flag work
- replace deprecated datetime.utcnow() with timezone-aware
datetime.now(timezone.utc)
- drop unnecessary is-not-None guard on non-optional
service.completion_model_id
- pass already-typed local IDs to ModelMigrationHistory
* fix: regenerate schema and align frontend models endpoints
- regenerate schema.d.ts from current backend openapi.json
- align getUsageDetails return type with PaginatedResponse
(the backend's local ModelUsagePaginatedResponse alias is
reduced to PaginatedResponse at schema-generation time)
- use ts-expect-error for query params on endpoints whose
query is read via a custom Depends() that doesn't surface
in the OpenAPI schema
- restore JSDoc on migrateCompletion so its parameters are
typed instead of any
- drop now-unused ts-expect-error in model-providers.listModels
- align WizardData.models.securityClassification with the
intric-js SecurityClassification type to fix a structural
mismatch with StepModels' models prop
- type the wizard's intermediate created model and replace
the open any cast on the security_classification update
- add missing migrated_to_model_id and deleted_at fields on
the SimpleNamespace test fixture to match the new domain
CompletionModel
* feat(backend): add per-token cost tracking with LiteLLM enrichment
Adds NUMERIC(20,12) input/output_cost_per_token columns to completion and
embedding model tables, and NUMERIC(20,6) cost_per_minute to transcription
models. Domain/presentation models, assemblers, and the model-providers
capability flow are extended to surface cost from LiteLLM's model_cost map
both for the live catalog enrichment and the static \`/capabilities/\` endpoint.
A backfill alembic step fills NULL costs for existing rows by looking up
the model in LiteLLM with the same prefix-fallback resolution as the
runtime defaults endpoint.
Also fixes \`deleted_at\` on completion models to map as \`DateTime(timezone=True)\`
so tz-aware UTC datetimes no longer crash the asyncpg soft-delete path.
* feat(backend): MODEL_IN_USE (9039) error + scope deletion blockers to actual usage
Introduces a dedicated \`ModelInUseException\` mapped to error code 9039 so the
frontend can localize the message and offer the migrate flow inline instead
of swallowing a generic 400. The four delete paths (tenant completion /
embedding / transcription routers and the sysadmin counterpart) now raise
this typed exception consistently.
Also narrows what blocks completion-model deletion to actual resource
references (assistants, apps, services, assistant/app templates). Space
membership alone no longer blocks — spaces are containers, and a model
"enabled" on a space without any resource using it is just configuration.
The cross-reference rows are cleaned up automatically inside
\`delete_model\` so soft-deleted models don't dangle in
\`Spaces.completion_models\` reads.
* feat(frontend/admin-models): redesign add wizard, cost-aware UX, shadcn dialogs
Consolidates the legacy per-type dialogs (AddEmbeddingModelDialog,
AddTranscriptionModelDialog) into a single AddWizard that handles
completion, embedding, and transcription models with a stepper, draft
list, suggestions catalog, and provider/credentials/security steps.
Adds end-to-end pricing UX: ModelCostBadge in tables, Cost row in the
detail dialog, and a per-1M-tokens display in the wizard/edit forms with
auto-fill from the LiteLLM defaults endpoint. Storage stays as USD/token.
Migrates the model dialogs to shadcn: EditModelDialog, MigrateModelDialog,
ModelDetailDialog, the delete confirm in ModelActions, and the wizard.
Hooks the new MODEL_IN_USE (9039) error through getErrorMessage so the
delete confirm renders a localized message and offers the migrate flow
inline. Migrate dialog skips the resource warnings (different_family,
kwargs_reset, …) when no resources actually use the model — the
warnings are vacuous in that case. Splits the form-reset effect so an
upstream availableTargets refresh can no longer wipe an in-progress edit.
Clarifies the deprecation banner / tooltip to read "should be migrated
or removed" since deletion is now a first-class option for unused models.
Regenerates the OpenAPI schema for the new ErrorCodes value.
* feat(frontend/admin-usage): show estimated USD cost alongside token totals
Pulls the new per-token costs from the model list and surfaces an
estimated USD cost in the tenant token-usage views. Adds an
EstimatedCostCell renderer to the token overview table and pipes a
shared cost-rate map through the user breakdown, the per-user detail
view, and the summary cards. Models without a published rate fall back
to "—" rather than zero so unknown costs aren't silently rolled into
totals.
* fix: stabilize admin model migration flows
* refactor(frontend/admin-models): drop provider status badge, polish empty state and form placeholders
Removes the auto-test ProviderStatusBadge component and its i18n keys —
the badge surfaced too rarely useful state to justify the on-click round
trip to upstream providers, and was visually noisy in tables with many
configured providers.
Other polish in the same surface:
- PageEmptyState now accepts custom title/description/ctaLabel/helper
props so it can be reused for other empty contexts beyond the
zero-providers case.
- ModelDraftForm cost inputs use a localised cost_input_placeholder
instead of hardcoded sample numbers (matches Swedish translations
more naturally and avoids implying specific pricing).
- ProviderDialog footer becomes a plain div to match the AddWizard
footer styling rather than the shadcn Dialog.Footer default spacing.
* feat(frontend/admin-models): hide providers without models in current section
A provider only shows up on a tab (Completion / Embedding / Transcription)
once it has at least one model in that section. Previously every
configured provider rendered an empty row in every tab regardless of
whether it actually served that model type, which created visual clutter
and implied capabilities the provider hadn't been set up for.
The "Add provider" CTA still routes through the AddWizard, where the
"Use existing provider" tab lets the user reuse a configured provider
and just add a model to it — at which point the provider appears in the
section.
Adds a focused empty state for the case where the tenant has providers
configured but none with models in this section, pointing at the same
"Add provider" CTA.
* feat(tenant-models): set security classification at create time
Lets admins assign a security classification while creating a tenant
model in the AddWizard rather than via a separate post-create dialog.
The bulk-classification flow on /admin/security-classifications is
removed; classification is now a column on the model table and editable
through the per-model dialog.
Backend:
- Add `security_classification: ModelId | None` to the three
TenantXxxModelCreate schemas (completion / embedding / transcription)
- Extract `resolve_tenant_security_classification` into
`intric.security_classifications.tenant_validation` so all three
routers share one tenant-isolation check
- Switch the update routers to `model_fields_set` for nullable fields
(description, dimensions, max_input, cost columns) so clients can
clear them by sending an explicit `null`
- Drop the placeholder "Tenant model: X" description fallback in the
completion router; store what the admin sent, matching embedding
and transcription
- Hoist inline imports to module level for consistency
Frontend:
- AddWizard sends `security_classification` with the create payload
instead of a follow-up update call
- ProviderGroupedModelTable shows a classification column when the
tenant has any classifications configured
- New ModelClassificationCell renders the value (or "no classification")
- Remove ModelClassificationDialog + MultipleModelsClassificationDialog
and related strings
- Regenerate schema.d.ts
* test(tenant-models): cover security classification + nullable update flows
Unit tests exercise `resolve_tenant_security_classification` directly
(none/match/missing/cross-tenant) so the helper has fast regression
coverage that runs in the devcontainer.
Integration tests run end-to-end against the three create/update
endpoints. Notable scenarios:
- Cross-tenant classification id rejected as 404 — the most important
isolation guarantee, exercised from each modality
- description is stored verbatim (no placeholder), guarding the
placeholder removal in the completion router
- nullable fields (description, cost columns, dimensions, max_input,
cost_per_minute) are clearable by sending an explicit `null`
- omitted fields are preserved on update — the regression guard for
any future revert from `model_fields_set` to `is not None`
- a model in another tenant cannot be reached through this tenant's
update endpoint
* refactor(admin/models): validate cost input client-side and document DB caps
Admins enter token cost as USD-per-million in the AddWizard / Edit
dialog. The values are stored as `Numeric(20, 12)` per token (so
< 10^8 USD/token, < 10^14 USD/million-tokens) and per-minute audio
cost as `Numeric(20, 6)` (< 10^14 USD/minute). The form needs to
catch overflow before the DB rejects it, otherwise admins get a
500 with no useful message.
- Add `MAX_COST_INPUT = 99_999_999_999_999` and helpers
`findDraftCostOverflow` / `isCostValueOverflow` in `draft.ts`,
shared between the wizard and EditModelDialog
- Export `rawCostToNumber` and reuse it from EditModelDialog instead
of duplicating the same parser
- Wire `<input type="number" max={MAX_COST_INPUT}>` and a translated
toast/error so the limit is visible in the UI
- Cross-link the constant with the SQL column definitions in
`ai_models_table.py` so a future precision change has to update
both sides
* refactor(ai-models): unify ModelNameAndVendor description modes
Replace the dual `showDescription` + `descriptionTabbable` props with a
single `descriptionMode: "interactive" | "non-tabbable" | "hidden"`
prop. The previous booleans had three valid combinations; the new enum
makes that explicit and removes the redundant fourth state.
The component now also renders the model description as a focusable
info button instead of a tooltip on the model name itself, so the
description is reachable both visually and via screen reader without
breaking nested-interactive-content rules in cells/listbox options.
* feat(migrate-dialog): block migration when impact load fails
Previously, when `getUsageStats` failed for the source model, the dialog
silently set `spacesCount = 0` and let the admin migrate against an
incomplete impact summary. The backend's
`get_model_usage_statistics` returns 200 with zero counts for models
without recorded usage (verified in
`completion_model_usage_service`), so a failure here always means a
real backend or network problem.
Now the dialog surfaces an explanatory error with a retry button and
disables the migrate action until impact data has loaded. The admin
can no longer click "migrate" while staring at a partial summary.
* fix(frontend): assorted UX and DX polish
Bundles small unrelated improvements that landed alongside the security
classification work:
- vite.config.ts: allow the dev server to serve files from the
monorepo root so workspace icons load without "outside of allow
list" errors
- intric-js client.js: throw a descriptive error when a path
parameter is `undefined` / `null`, instead of producing a URL
containing the literal string "undefined"
- Markdown.svelte / ReferenceContext.ts: turn `renderer` into a
getter so the custom in-text-reference renderer reacts to changes
in the host context
- ExtendExpirationDialog.svelte: initialise `pickerValue` to null and
seed via `$effect` to avoid `state_referenced_locally`
- admin/templates "deleted" tab: use dedicated empty-state strings
instead of the generic "no templates yet" message
- Hide cost badges in the user-facing model selectors (apps,
assistants, services edit pages); cost remains visible in admin
- estimated_cost_tooltip clarified — reasoning tokens are billed as
regular output, matching how providers actually charge
* feat(audit): add CREATED and DELETED action types for AI models
The audit vocabulary previously only had `*_MODEL_UPDATED` for the
three model types, so adding/removing tenant models was invisible in
the audit trail. Adds:
- COMPLETION_MODEL_CREATED / _DELETED
- EMBEDDING_MODEL_CREATED / _DELETED
- TRANSCRIPTION_MODEL_CREATED / _DELETED
All six map to the `user_actions` category (matching the existing
`*_MODEL_UPDATED` mapping) and ship with Swedish admin-config labels
so the audit-config UI keeps working without manual seeding.
Updates the hardcoded category-count assertions in the audit
mapping/config tests from 30 → 36.
* refactor(tenant-models): extract service layer for create/update/delete
The three tenant routers (completion / embedding / transcription) were
mostly inline SQLAlchemy: provider lookup, default-unsetting,
classification resolution, model construction and audit-friendly
loading all sat in the request handler. That made cross-cutting
concerns hard to share, hard to test in isolation, and inconsistent
with how the global model routers compose business logic via
container-resolved services.
This change introduces `intric.tenant_models.application.tenant_model_service`
with three classes (one per model type) that share helpers via
composition:
- `_validate_active_provider` — uses ModelProviderRepository.get_by_id
for tenant-scoped lookup, raises BadRequest on disabled providers
- `_unset_other_defaults` — generic over the table class, called only
when the new/updated model is being promoted to default
- `_resolve_tenant_security_classification` — already extracted
- `_audit` — log_async wrapper with the standard metadata shape
- `_DeletedSnapshot` — small id+name carrier so delete-time audit
metadata isn't lost when SQLAlchemy detaches the row
Audit logging is now wired for create / update / delete on all three
modalities (six new action types added in the previous commit). The
update logs the set of fields that were actually changed, so audit
viewers can distinguish "renamed model" from "rewrote ratecard".
The routers themselves drop to ~20 lines each: validate permission,
build the service, call one method, commit the session, return.
* feat(security-classifications): block deletion when classification is in use
The classification-id FK on completion / embedding / transcription
models, spaces and MCP servers is `ON DELETE SET NULL`, so deleting
a referenced classification used to silently downgrade every dependent
row to "no classification". For a system whose whole point is gating
model availability by classification, that's an accidental privilege
escalation: the admin clicks "delete", sees the row gone, and only
notices the loosened access in production traffic.
Changes:
- `SecurityClassificationRepoImpl.count_usages(id)` returns per-table
reference counts (completion/embedding/transcription/spaces/mcp)
- `SecurityClassificationService.delete_security_classification` now
refuses by default if any usage > 0, with an error message naming
the dependent resource counts so the admin can act on it
- `force=True` is the explicit opt-in for the old behaviour; passed
via `?force=true` query param on the router
- The audit metadata records `forced=true|false` so an audit reviewer
can see when an admin overrode the guard
Tests cover all three branches (blocked-when-in-use, passes-through-
when-unused, force-skips-check) at the service level, plus an
integration test that creates a tenant model + classification and
verifies the 400 vs the 204+force pathways.
* feat(conversations): preflight token estimate endpoint with live context bar (#404)
Adds POST /api/v1/conversations/preflight which returns the exact token
delta the next chat request would add (input + file_tokens), echoing the
target model name + context window so clients can compute percentage fill
locally.
Backend:
- Extract shared _ConversationTarget mixin for the "exactly one of
session/assistant/group_chat" validator (drops duplicate between
PreflightRequest and ConversationRequest)
- Route through new _resolve_completion_model so preflight uses the same
model the actual chat will, with explicit return type and tested coverage
for the session-with-group_chat-id path
- Rate-limit at 600 req/min/user (400ms debounce yields ~150/min in normal
use; limit catches scripted abuse, fail-open on Redis outage)
- Validate input: max 50 file_ids per request, reject empty (no question
and no files) with 422
- Remove unused get_files_for_token_estimate / get_list_by_id_and_tenant
- Document Message.num_tokens_* semantics: default 0 covers pre-measurement
rows, clients summing these should treat 0 as "zero OR unmeasured"
Frontend:
- ChatService tracks lockedInput/Output (from token_usage SSE), pending
preflight tokens (debounced 400ms, race-safe via generation counter) and
exposes willExceedContext as the single source of truth
- ContextUsageBar shows segmented fill (locked vs pending, four distinct
WCAG-compliant hues), opens popover with full breakdown + cumulative
spend, persists visibility preference
- ConversationInput blocks Send when willExceedContext so the bar and the
button can't disagree
- Augment ConversationMessage with num_tokens_* and PreflightResponse type
hand-authored against schema (will resolve on next openapi-typescript regen)
Tests:
- tests/unittests/conversations/: 20 unit tests covering rate-limit 429,
fail-open, scope 403, exact token math against build_files_string output,
group-chat routing, empty-input rejection, max_length cap
i18n:
- context_usage_* keys for the bar UI in en/sv
.gitignore:
- Ignore local review notes and ad-hoc UI screenshots
* fix(tests): align cost-tracking unit tests + merge dual alembic heads
Cost-tracking commit (3260dd0a) added input_cost_per_token /
output_cost_per_token on the domain model and in the LiteLLM enrichment
helper, but three tests still asserted the pre-cost shape.
The recent merge from develop also brought in 202605061100, leaving
20260501_backfill_model_costs and 202605061100 as two heads diverging
from 202604291030. Add an empty merge migration to collapse them.
* fix(migrations): disambiguate LiteLLM cost backfill + tighten sessions downgrade
20260501_backfill_model_costs previously picked the alphabetically-first
provider prefix when a model name appeared under multiple providers in
LiteLLM, silently writing wrong prices (e.g. Azure-served gpt-4o getting
OpenAI prices because azure < openai). It now JOINs model_providers to
resolve provider_type per row and prefers the matching prefix; global
models with ambiguous prefixes are skipped with a reported count instead
of being guessed.
202605061100 downgrade previously dropped api_key_id before the
SET NOT NULL on user_id, which left operators with no way to identify
service-key sessions if the constraint re-application failed. The order
is now: purge NULL-user_id rows → SET NOT NULL → drop column.
* fix(sysadmin): convert force-delete IntegrityError to MODEL_IN_USE (400)
20260402_lifecycle switched questions.completion_model_id to RESTRICT so
historical attribution cannot be silently erased. The sysadmin
delete_completion_model(force=true) path issues a raw sa.delete, which
now raises IntegrityError for any model with question history. Surfacing
that as a 500 leaves operators with no actionable signal; catch and
re-raise as ModelInUseException so the response carries the same 400 +
MODEL_IN_USE (9039) code the soft-delete path already uses.
Adds integration tests covering both force=true paths (with/without
question history).
* refactor(users): stop auto-provisioning personal API keys at user creation
Every user created via /admin/users or /sysadmin/users/ silently received an
ACTIVE admin-permission sk_ key in api_keys_v2. The plaintext was returned in
the HTTP response but no frontend client read it (UserEditor.svelte ignored
the api_key field), so the keys were minted, persisted, and forgotten — an
unused admin-scope credential per created user with no way for the user
themselves to ever see the value.
Removes:
- create_user_api_key_v2 from auth_service (no remaining callers)
- the api_key field from UserCreatedAdminView
- the api_key tuple slot from user_service.register()'s return shape
- the api_key= kwarg in sysadmin/admin user-creation responses
Users mint their own API keys on demand via POST /api/v1/api-keys, which is
the only flow that actually surfaces a usable plaintext value.
* fix(audit, migration-service): demote chatty WARNINGs + swallow audit enqueue failures
Migration service: the MIGRATION_DEBUG [N/7] step logs were emitted at WARNING
level on every successful migration, polluting alerting channels and leaking
tenant_id/user_id on benign progress events. Demoted to INFO and dropped the
"MIGRATION_DEBUG" prefix that betrayed leftover debug instrumentation. The
genuine warning paths (user overriding compatibility issues, unknown entity
types) keep their WARNING level.
Audit service: log_async now treats Redis/ARQ enqueue failures as best-effort.
A transient Redis outage previously propagated and 500'd every audited
mutation. The handler now logs a warning, returns None, and lets the caller
proceed — turning a partial degradation back into a partial degradation.
* fix(db): build new completion-model indexes CONCURRENTLY + enforce sessions XOR
20260403 added two indexes on completion_models without CREATE INDEX
CONCURRENTLY. On a large completion_models table that would lock the table
for the duration of the build. Wrap the index creation (and the corresponding
drops in downgrade) in autocommit_block + raw CONCURRENTLY SQL, mirroring the
pattern from 202604221200.
Adds a new migration 20260512_sessions_xor_check that enforces "exactly one
of sessions.user_id / sessions.api_key_id is set" at the DB layer. The
invariant was previously documented in sessions_table.py but only upheld by
calling code, so a future regression (or manual tweak) could silently land
a NULL/NULL row that no list query would surface. The ORM mapping picks up
the same CHECK so model-driven setups stay in sync.
* Revert "refactor(users): stop auto-provisioning personal API keys at user creation"
This reverts commit 14a41524c3f31b0a844a1029e0ad6e470b798cce.
* fix(migrations): shorten merge-heads revision id to fit varchar(32)
alembic_version.version_num is varchar(32). The merge-migration's
revision id 20260512_merge_cost_and_api_key_heads is 37 characters,
so any UPGRADE that lands at that node fails with
StringDataRightTruncation when alembic tries to write the new
version_num. The dependent xor-check migration carried the same
oversize identifier as its down_revision.
Rename both occurrences to 20260512_merge_heads (20 chars). Filename
unchanged.
* refactor(migrations): linearize sessions branch + inline XOR check
The branch had two follow-up migrations patching another migration that
landed on the same branch — i.e. migrations-of-migrations that never
need to exist in history because nothing outside this branch has run
them yet.
- 20260512_merge_heads only existed to converge two heads that diverged
from 202604291030 (sessions-nullable vs cost-backfill). Re-parent
202605061100 onto 20260501_backfill_model_costs so the chain is
linear and the merge node is unnecessary.
- 20260512_sessions_xor_check just added a CHECK constraint to the
sessions table that 202605061100 itself had introduced. Fold the
constraint into 202605061100 since the invariant is part of the
same design ("a session is either user-owned or service-key-owned").
Result: one head (202605061100), no merge migration, no follow-up
patch migration. ORM-side CheckConstraint in sessions_table.py is
unchanged.
* fix(model-defaults): provider-aware lookup + drop preflight ts-ignore
Extract a shared resolve_model_defaults() helper used by both the
/model-defaults/ endpoint and the cost-backfill migration so the wizard's
"Lookup defaults" button and the bulk backfill agree on which LiteLLM row
maps to a given (name, provider_type) pair. Without the provider hint,
Azure-served gpt-4o silently resolved to openai/gpt-4o prices.
The endpoint now accepts provider_type as an optional query param. The
wizard threads providerType through ModelDraftForm; the edit dialog uses
the model's persisted provider_type when available. Ambiguous names
without provider context return found=false and are skipped by the
backfill so admins can disambiguate via the UI instead of receiving a
silently-wrong price.
Regenerating schema.d.ts also makes /conversations/preflight typed, so
the handwritten PreflightResponse alias in resources.d.ts and the
@ts-ignore in conversations.js are gone.
* fix(tenant-models): fold is_default + classification into tenant update
The edit dialog used to make two backend calls per save: first the
tenant update endpoint, then a legacy /models/{id} update for
is_default and security_classification because the tenant routes
didn't accept them. If the second call failed the user saw the
display name persist while the classification "saved" toast lied.
Tenant*ModelUpdate now accepts both fields. The service applies them
under the same transaction as the rest of the update — `is_default=True`
unsets sibling defaults via the same helper used at create time, and
`security_classification` runs through the cross-tenant guard in
intric.security_classifications.tenant_validation. Sending an explicit
`null` clears the classification (matching the existing
model_fields_set semantics for nullable fields).
Frontend: EditModelDialog merges the two fields into the build*Update()
payloads and drops syncSecurityAndDefault(). The legacy intric.models.update
fallback is no longer reachable from this dialog. The hidden
is_org_default check stays so transcription models (no UI toggle today)
don't accidentally send a stale default flag.
Tests:
- Drop the now-redundant test_backfill_model_costs_lookup.py (the shared
resolver tests in test_model_defaults_lookup.py cover the same matrix).
- Add integration tests for: promoting default unsets sibling, set+clear
classification, cross-tenant classification rejected on update, and a
combined save that mutates display name + default + classification in
one request. Mirror tests for embedding + transcription.
* refactor(completion-models): slim router migration + usage handlers
The migration + usage endpoints had ~150 lines of scaffolding that
duplicated work already done elsewhere:
- Local `import logging` and `getLogger(__name__)` in three handlers.
Hoisted to module level.
- Local `from fastapi import HTTPException` repeated four times to
re-validate `model_id is None`, `to_model_id is None`,
`user.tenant_id is None`. FastAPI's path-param coercion and the
current_user dependency already guarantee those — the manual checks
would only fire on a bug we'd want to see as a real 500, not a
silently-rewritten 400.
- Same-model rejection lived in both the router and the service.
Dropped from the router; CompletionModelMigrationService.migrate_model_usage
already raises ValidationException with a richer error message that
includes the conflicting model name.
- Catch-all `except Exception: logger.error(...); raise` wrappers that
added nothing FastAPI's own exception handlers don't already log,
with the side effect of double-emitting errors at WARN+ERROR.
Kept the targeted try/except around the audit log call — a failed audit
must not break the user-facing response after the migration committed.
Validation: pyright clean, 79 completion_models unit tests pass.
* refactor(worker): explicit per-job session ownership for cleanup loop
The lifecycle cleanup job had its own session-management quirk: the
cron wrapper already opens a session+transaction, but cleanup opened a
SECOND session and overrode the container's session provider with
`cast(Any, container.session).override(providers.Object(session))`.
The outer transaction did nothing while the inner one held the actual
work, and the cast leaked the dependency_injector internals across an
infrastructure boundary. Easy to copy wrong and hard to reason about.
Worker.cron_job now accepts `manages_own_session=True`. With that flag
the wrapper still hands the job a configured container but skips the
implicit `session.begin()` — the job manages per-batch transactions
itself. Default behaviour (one outer transaction) is unchanged so the
nine other cron jobs are unaffected.
Cleanup job changes:
- Opts in via `manages_own_session=True`.
- Drops the second sessionmanager.session() and the cast/override dance.
- Adds a 500-row batch cap with deterministic ordering so a backlog
drains over multiple weekly runs rather than locking the table once.
- Drops the now-unused sessionmanager + dependency_injector.providers
imports.
Tests:
- Update test container shape to the new contract (a callable that
returns the session) and drop the sessionmanager monkeypatch.
- Reach past the cron wrapper via __wrapped__ so unit tests exercise
the cleanup logic without dragging in arq context or the real
sessionmaker.
- Add a regression guard that asserts the candidate query runs on the
container-provided session. If a future refactor re-introduces the
override pattern, this test fails.
Validation: pyright clean, 1489 unit tests pass.
* polish(admin-models): localise migration history + stabilise deprecation test
Drop the last few hand-rolled strings + a calendar time-bomb the PR
review flagged:
- MigrationHistoryPanel: the load-failure fallback was a hardcoded
English string. Status badges rendered the raw backend value
("completed", "in_progress") instead of a localised label. Both now
go through the paraglide message layer with unknown statuses falling
back to the raw key so a future backend addition doesn't blank the
badge.
- StepModels unsupported-provider warning: the alert interpolated raw
`providerType` ("hosted_vllm") and raw `modelType` ("transcription")
into the message. Threaded them through formatProviderLabel() and a
small modelType → m.model_type_* map so the user sees "vLLM" and
"transcription" rendered the same way as elsewhere in the wizard.
- test_deprecation_lookup: the future-date fixture was a hardcoded
"2026-12-01" + assertions like "this is in the future". That test
starts failing the day the calendar passes 2026-12-01 with no code
change. Replaced with date.today() ± 365 days so the past/future
distinction stays stable indefinitely.
No new English-only strings; sv.json and en.json kept in sync.
* chore(frontend): format space settings page
Drops a stray multi-line filter that prettier wanted on one line.
Pre-existing formatting drift caught when running bun run lint
end-to-end during the Fas 6 cleanup pass.
* fix(model-migration): persist failure history
* fix(tests): drop manual commit inside db_container session.begin block
The db_container fixture already opens a session.begin() block that
commits on exit. Calling session.commit() inside closes the
transaction early, after which the trailing ORM attribute read (or
the block's own __aexit__) crashes with InvalidRequestError. Removing
the manual commits lets the surrounding transaction commit normally
and the rows are visible to the subsequent HTTP requests through the
shared sessionmanager.
* style: apply ruff format to three modified files
* fix: enforce effective model deprecation
* fix(chat): guard tool SSE handlers against pre-first-chunk events
onToolCall and onToolApprovalRequired read ref.mcp_tool_calls without
verifying ref is defined. ref is only assigned inside onFirstChunk, so a
tool_call or tool_approval_required event arriving before the first text
chunk crashes the entire stream with "Cannot read 'mcp_tool_calls' of
undefined".
Both handlers also invoked ensureCurrentSession but ignored its return
value, leaving them able to mutate a message belonging to a previous
conversation if the user switched sessions mid-stream.
Apply the same ref+isStale+ensureCurrentSession guard that onText,
onImage, onIntricEvent and onToolApprovalTimeout already use.
* fix(auth): validate resolved client IP so audit INET column never sees garbage
resolve_client_ip returned request.client.host unchanged. Starlette's
TestClient sets that to the literal "testclient", and proxies can inject
non-IP values in X-Forwarded-For or X-Real-IP. Both end up flowing into
the audit_logs.ip_address INET column (worker insert crashes with
DataError) and the API-key allow-list parser (which calls
ipaddress.ip_network and silently treats the match as a miss).
Validate every extracted candidate against ipaddress.ip_address(); on
ValueError return None so callers treat the IP as unknown rather than
storing or comparing garbage. The api_key_policy _validate_ip path
already raises 403 "Client IP unavailable" on None, which is the right
fail-closed behaviour when an IP-restricted key cannot identify the
caller.
Tests cover non-IP hostnames, proxy-injected garbage in X-Forwarded-For,
IPv6 happy path, and the middleware end-to-end propagation.
* fix(auth): close disable_grace_period microsecond race
api_key_lifecycle sets rotation_grace_until = datetime.now(timezone.utc)
when the rotation request opts out of the grace window. The downstream
compare in compute_effective_state used strict less-than, so a request
arriving on the same microsecond would compute as still inside grace and
authenticate with the now-rotated secret one more time.
Make the grace comparison inclusive (<=) and add a dedicated unit test
for the exact-at-now case plus a paired ACTIVE-during-grace control.
The existing integration test for disable_grace_period only checked the
DB field; extend it to also verify that a real HTTP request with the
old secret receives 401 after rotation.
* fix(admin-models): block EditModelDialog submit on missing token budgets
handleSubmit only checked displayName and cost overflow before posting,
so clearing max_input_tokens or max_output_tokens on an existing
completion model sent null to the backend — which rejects it, and any
downstream context-window math would have divided by zero anyway.
Extract the existing AddWizard rule (isDraftComplete) into a shared
hasValidCompletionTokenBudgets helper in draft.ts, then reuse it from
both the wizard and the edit dialog so the two places cannot drift.
* fix(auth): close legacy pk_ origin bypass when allowed_origins is NULL
_validate_origin treated NULL allowed_origins as "permit every origin",
intended as a back-compat shim for legacy pk_ rows minted before the
field became required. The same passthrough also fires for any future
regression that lets a row land with allowed_origins=NULL — silently
disabling CORS-style origin enforcement for that key.
Fail closed: NULL and empty list both raise origin_not_allowed (the same
error path empty-list already used). Legacy keys that ended up NULL need
to be rotated with an explicit origin list; the existing test that
asserted the permit-legacy branch is inverted to lock in the closed
behaviour.
Production impact: any pk_ key still carrying allowed_origins=NULL stops
authenticating. The deliberate trade-off is that this small population
of misconfigured keys was already a CORS bypass — failing them loudly
is preferable to silently accepting any origin.
* docs(model-migration): document partial-state + failure-history edge cases
Two related migration edge cases that surfaced during review but turned
out to be deeper than a single-commit fix:
1. Selective entity_types — when "assistants" is passed without "spaces"
the source model is enabled-then-orphaned on SpacesCompletionModels.
The frontend always sends undefined (= all types), so this is only
reachable via direct API calls and the dangling row is cosmetic
(filtered by migrated_to_model_id checks and cleaned up by the
model-cleanup worker on hard delete). Documented inline so future
API-direct callers see why selective migrations skipping spaces are
accepted but discouraged.
2. SQLAlchemyError except-block — writes migration_history through the
same session whose outer transaction has already aborted, so true DB
failures (deadlock, connection loss) silently lose the failed-row
that the surrounding contract promises to persist. A proper fix
needs a separate session via DI; flagged as a follow-up so we don't
risk happy-path regressions in this round.
* fix(sysadmin): map embedding force-delete IntegrityError to MODEL_IN_USE
The completion-model force-delete branch wraps the destructive DELETE
in a try/except that translates IntegrityError into MODEL_IN_USE (400),
so operators get a meaningful response when an FK still references the
model. The embedding-model branch had the same kind of risk (Websites
and IntegrationKnowledge reference embedding_model_id without ondelete)
but let the IntegrityError bubble up as a 500. Apply the same wrapper.
Also flag the four sysadmin model-lifecycle endpoints with an inline
AUDIT GAP note. audit_logs.tenant_id is NOT NULL, but a global model
row has no owning tenant at create/delete time, so emitting an audit
row from these paths would require either a system-tenant convention
or a separate sysadmin-only audit store. Tenant-scoped lifecycle
(tenant_model_service) already audits CREATED/DELETED correctly — the
gap is only the cross-tenant sysadmin path, which is reachable solely
via ENEO_SUPER_API_KEY and traced through app logs today.
* fix(security-classifications): exclude soft-deleted models from in-use check
count_usages drove the "cannot delete, X resources still reference this
classification" gate, but completion_models retain their FK after
soft-delete. The admin would then see e.g. "3 completion_models in use"
with no live model to actually reassign. Filter on deleted_at IS NULL
so the count reflects only what the admin can act on.
Also document the count → delete TOCTOU window inline. A new row linked
to this classification between the two statements gets cleared to NULL
via ON DELETE SET NULL — but NULL is the default/restrictive state, so
the damage is operational (admin reassigns) rather than a privilege
escalation. Postgres does not propagate row-level locks across FK
checks, so SELECT FOR UPDATE on the classification would not block the
racing INSERT — closing the gap properly needs SERIALIZABLE isolation,
deferred until we see it in practice.
* fix(audit): reraise programming errors in audit enqueue instead of swallowing
The bare `except Exception` swallowed every failure from job_manager.enqueue
so that an unreachable Redis would not turn a successful mutation into a
500. That contract is right, but it also hid TypeError on non-serialisable
params, ValueError on invalid input, and AttributeError / AssertionError
from refactors — bugs that should surface in dev/CI rather than vanish
into a warning log.
Catch the four programming-error classes ahead of the broad handler and
let them propagate. Genuine infrastructure failures (Redis/ARQ
unreachable, network timeouts) still hit the swallowing branch.
* fix(model-migration): restore WARNING level for rejected validation
Validation-rejected migrations were demoted from WARNING to INFO when the
MIGRATION_DEBUG step logs were demoted, which buried a real "system
blocked this admin action" event in normal success-step output. Operator
alarm rules that watched WARN on this message stopped firing.
Restore WARNING for the rejection branch only — the per-step success
logs stay INFO since they fire on every migration run.
* fix(admin-models): handle partial failure in AddWizard createModels loop
The wizard's createModels iterated sequentially with a bare for-loop, so
the first throw aborted the run with no toast, no closure, and the
already-created rows still sitting in wizardData.models. The next submit
attempt hit duplicate-name 400s from the backend with no way out of the
dialog except cancel.
Track succeeded vs failed per model, drop the succeeded ones from
wizardData.models so a retry only resubmits what actually failed, and
branch on the outcome:
- All succeeded: existing success toast + close.
- All failed: re-raise the first error so the existing catch sets the
error banner and keeps the dialog open.
- Partial: warning toast naming the failing models; failed rows stay in
the list for the user to fix in place.
New i18n key `models_partially_created` in en + sv.
* polish: trim historical narrative from comments left by review fixes
Code comments should carry forward value (invariants, edge cases,
TODOs, cross-references) — not narrate what a diff changed.
Reviewers and future readers get the history from git log; the
inline comments should explain the contract that exists now.
* docs(api-keys): document per-key origin trust model
pk_ allowed_origins is the only authority — no tenant-wide allowlist
overrides individual keys. Make the trust model explicit so operators
understand that key creators choose origins, and that a NULL/empty list
is rejected at auth rather than silently permitting any origin.
* fix: small review follow-ups across model migration, preflight, and downgrade
- Audit description for COMPLETION_MODEL_MIGRATED now resolves the target
model name instead of echoing the UUID, so the operator-facing log
string reads "from A to B" rather than "from A to <uuid>".
- Legacy v1 -> v2 API-key migration: clarify the inline comment.
Mapping non-admin owners to tenant+write does not actually let the
migrated key authenticate (the resolver's owner-admin guard still
rejects it); we preserve the row for data continuity, the key only
becomes usable if the owner is later promoted.
- Preflight: surface `excluded_file_count` so callers know image/binary
attachments contributed unknown tokens. Tokens already exclude them;
this just makes the omission explicit in the response contract.
- Sessions downgrade migration: count and log the service-key sessions
about to be purged so operators see the data-loss footprint before
the DELETE runs. The CASCADE down to questions is irreversible.
* fix(alembic): inline model-defaults resolver in cost backfill migration
The backfill migration imported intric.model_providers.domain.model_defaults_lookup
at module top, which makes a fresh-DB alembic upgrade fragile: any future
rename or move of that module would break replaying old migrations.
Inline the two functions (resolve_model_defaults, is_ambiguous) as module-
local helpers. The runtime module stays the single source of truth for
the wizard's "Lookup defaults" button; the migration carries a frozen
copy for replay safety. A comment in both files flags that they must
stay in sync if either changes.
* polish(auth): drop redundant grace-comparison comment
The inclusive <= behaviour is local enough to read from the code itself.
* fix(auth): close pk_ allowed_origins null gap on update path
validate_create_request rejects allowed_origins=None for pk_, but
validate_update_request only blocked the empty-list case. Frontend
edit form sent `null` when an admin cleared the origin list, so the
PATCH landed and the next request hit the fail-closed branch in
_validate_origin — locking the key out the moment it was edited.
Mirror the create-path rule on update (None or [] → 400). Frontend
now blocks the submit and surfaces api_keys_origin_required, so the
round-trip is short-circuited before the backend ever rejects it.
Adds test_update_request_rejects_null_allowed_origins_for_pk to lock
the new behaviour in.
* fix(admin-models): clear pendingDraft on partial create success
finishWizard pushes pendingDraft into modelsToCreate by reference,
but the partial-failure cleanup only filtered wizardData.models.
If the unsubmitted draft succeeded while another row failed, the
draft stayed in the form — the next retry resubmitted it and the
backend rejected on duplicate name, defeating the whole point of
the partial-failure fix.
Filter pendingDraft against the succeeded list by identity in the
same block.
* fix(chat): materialize message when tool_approval_required arrives first
onToolApprovalRequired guarded against ref being undefined by
early-returning, mirroring onToolCall. For onToolCall that is
acceptable — the event is cosmetic and gets replayed. For an
approval event it is not: pendingToolApproval never gets set, the
approve/deny buttons never render, and the backend keeps waiting
for a decision that will never come.
When ref is missing, create the placeholder message inline (like
onFirstChunk does) and advance currentConversation.id from the
event. ref.session_id is not assigned directly — a later
onFirstChunk will Object.assign it, and the message itself does
not need session_id for the approval UI.
* fix(tests): add deprecation attrs to SimpleNamespace fixture
Assembler now reads is_effectively_deprecated and litellm_deprecation_date,
so the SimpleNamespace stand-in must expose both.1 parent 6c43e95 commit 975235b
191 files changed
Lines changed: 15616 additions & 6327 deletions
File tree
- backend
- alembic/versions
- src/intric
- ai_models
- completion_models
- embedding_models
- apps/apps
- assistants
- audit
- application
- domain
- authentication
- completion_models
- application
- domain
- infrastructure
- presentation
- conversations
- application
- database/tables
- embedding_models
- domain
- presentation
- files
- group_chat/application
- main
- container
- model_providers
- domain
- presentation
- questions
- security_classifications
- application
- domain/repositories
- presentation
- services
- spaces/api
- sysadmin
- tenant_models
- application
- transcription_models
- domain
- presentation
- worker
- tests
- integration
- completion_models
- sysadmin
- unittests
- ai_models
- apps
- assistants
- audit
- completion_models
- conversations
- model_providers
- security_classifications
- server
- unit
- frontend
- apps
- docs-site/src/content/docs
- web
- messages
- project.inlang
- src
- lib
- core/errors
- features
- ai-models
- components
- hosting
- api-keys
- chat
- components/conversation
- security-classifications/components
- routes/(app)
- admin
- models
- AddWizard
- models
- components
- security-classifications
- templates
- usage
- tokens
- users
- [userId]
- dashboard/[assistantId]/[[sessionId]]
- spaces/[spaceId]
- apps/[appId]/edit
- assistants/[assistantId]/edit
- chat
- services/[serviceId]
- settings
- packages
- intric-js/src
- client
- endpoints
- types
- ui/src/lib
- Dropdown
- Markdown
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
31 | 38 | | |
32 | 39 | | |
33 | 40 | | |
| |||
Lines changed: 134 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
Lines changed: 79 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
Lines changed: 79 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
0 commit comments